cmd, common, eth, node, rpc, whisper: combine node with RPC

This commit is contained in:
Péter Szilágyi 2015-12-02 14:29:37 +02:00
parent ff6b89b5d0
commit 3fb486a591
17 changed files with 372 additions and 109 deletions

View file

@ -398,7 +398,7 @@ func attach(ctx *cli.Context) {
client, err = comms.ClientFromEndpoint(ctx.Args().First(), codec.JSON) client, err = comms.ClientFromEndpoint(ctx.Args().First(), codec.JSON)
} else { } else {
cfg := comms.IpcConfig{ cfg := comms.IpcConfig{
Endpoint: utils.IpcSocketPath(ctx), Endpoint: (&node.Config{DataDir: ctx.GlobalString(utils.DataDirFlag.Name), IpcPath: ctx.GlobalString(utils.IPCPathFlag.Name)}).IpcEndpoint(),
} }
client, err = comms.NewIpcClient(cfg, codec.JSON) client, err = comms.NewIpcClient(cfg, codec.JSON)
} }
@ -506,11 +506,6 @@ func startNode(ctx *cli.Context, stack *node.Node) {
} }
} }
// Start auxiliary services if enabled. // Start auxiliary services if enabled.
if !ctx.GlobalBool(utils.IPCDisabledFlag.Name) {
if err := utils.StartIPC(stack, ctx); err != nil {
utils.Fatalf("Failed to start IPC: %v", err)
}
}
if ctx.GlobalBool(utils.RPCEnabledFlag.Name) { if ctx.GlobalBool(utils.RPCEnabledFlag.Name) {
if err := utils.StartRPC(stack, ctx); err != nil { if err := utils.StartRPC(stack, ctx); err != nil {
utils.Fatalf("Failed to start RPC: %v", err) utils.Fatalf("Failed to start RPC: %v", err)
@ -534,7 +529,7 @@ func accountList(ctx *cli.Context) {
} }
} }
// getPassPhrase retrieves the passwor associated with an account, either fetched // getPassPhrase retrieves the password associated with an account, either fetched
// from a list of preloaded passphrases, or requested interactively from the user. // from a list of preloaded passphrases, or requested interactively from the user.
func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string { func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) string {
// If a list of passwords was supplied, retrieve from them // If a list of passwords was supplied, retrieve from them

View file

@ -27,7 +27,6 @@ import (
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/rpc/codec" "github.com/ethereum/go-ethereum/rpc/codec"
"github.com/ethereum/go-ethereum/rpc/comms" "github.com/ethereum/go-ethereum/rpc/comms"
@ -37,7 +36,7 @@ import (
var ( var (
monitorCommandAttachFlag = cli.StringFlag{ monitorCommandAttachFlag = cli.StringFlag{
Name: "attach", Name: "attach",
Value: "ipc:" + common.DefaultIpcPath(), Value: "ipc:" + utils.IPCPathFlag.Value.Value,
Usage: "API endpoint to attach to", Usage: "API endpoint to attach to",
} }
monitorCommandRowsFlag = cli.IntFlag{ monitorCommandRowsFlag = cli.IntFlag{

View file

@ -23,7 +23,6 @@ import (
"log" "log"
"math" "math"
"math/big" "math/big"
"net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -40,25 +39,18 @@ import (
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/miner"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
"github.com/ethereum/go-ethereum/p2p/nat" "github.com/ethereum/go-ethereum/p2p/nat"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rpc/api" "github.com/ethereum/go-ethereum/rpc/api"
"github.com/ethereum/go-ethereum/rpc/codec" "github.com/ethereum/go-ethereum/rpc/codec"
"github.com/ethereum/go-ethereum/rpc/comms" "github.com/ethereum/go-ethereum/rpc/comms"
"github.com/ethereum/go-ethereum/rpc/shared"
"github.com/ethereum/go-ethereum/rpc/useragent"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
"github.com/ethereum/go-ethereum/whisper" "github.com/ethereum/go-ethereum/whisper"
"github.com/ethereum/go-ethereum/xeth" "github.com/ethereum/go-ethereum/xeth"
) )
@ -302,8 +294,8 @@ var (
} }
IPCPathFlag = DirectoryFlag{ IPCPathFlag = DirectoryFlag{
Name: "ipcpath", Name: "ipcpath",
Usage: "Filename for IPC socket/pipe", Usage: "Filename for IPC socket/pipe within the datadir (explicit paths escape it)",
Value: DirectoryString{common.DefaultIpcPath()}, Value: DirectoryString{"geth.ipc"},
} }
IPCExperimental = cli.BoolFlag{ IPCExperimental = cli.BoolFlag{
Name: "ipcexp", Name: "ipcexp",
@ -414,6 +406,15 @@ func MustMakeDataDir(ctx *cli.Context) string {
return "" return ""
} }
// MakeIpcPath creates an IPC path configuration from the set command line flags,
// returning an empty string if IPC was explicitly disabled, or the set path.
func MakeIpcPath(ctx *cli.Context) string {
if ctx.GlobalBool(IPCDisabledFlag.Name) {
return ""
}
return ctx.GlobalString(IPCPathFlag.Name)
}
// MakeNodeKey creates a node key from set command line flags, either loading it // MakeNodeKey creates a node key from set command line flags, either loading it
// from a file or as a specified hex value. If neither flags were provided, this // from a file or as a specified hex value. If neither flags were provided, this
// method returns nil and an emphemeral key is to be generated. // method returns nil and an emphemeral key is to be generated.
@ -602,6 +603,7 @@ func MakeSystemNode(name, version string, extra []byte, ctx *cli.Context) *node.
// Configure the node's service container // Configure the node's service container
stackConf := &node.Config{ stackConf := &node.Config{
DataDir: MustMakeDataDir(ctx), DataDir: MustMakeDataDir(ctx),
IpcPath: MakeIpcPath(ctx),
PrivateKey: MakeNodeKey(ctx), PrivateKey: MakeNodeKey(ctx),
Name: MakeNodeName(name, version, ctx), Name: MakeNodeName(name, version, ctx),
NoDiscovery: ctx.GlobalBool(NoDiscoverFlag.Name), NoDiscovery: ctx.GlobalBool(NoDiscoverFlag.Name),
@ -763,84 +765,6 @@ func MakeChain(ctx *cli.Context) (chain *core.BlockChain, chainDb ethdb.Database
return chain, chainDb return chain, chainDb
} }
func IpcSocketPath(ctx *cli.Context) (ipcpath string) {
if runtime.GOOS == "windows" {
ipcpath = common.DefaultIpcPath()
if ctx.GlobalIsSet(IPCPathFlag.Name) {
ipcpath = ctx.GlobalString(IPCPathFlag.Name)
}
} else {
ipcpath = common.DefaultIpcPath()
if ctx.GlobalIsSet(DataDirFlag.Name) {
ipcpath = filepath.Join(ctx.GlobalString(DataDirFlag.Name), "geth.ipc")
}
if ctx.GlobalIsSet(IPCPathFlag.Name) {
ipcpath = ctx.GlobalString(IPCPathFlag.Name)
}
}
return
}
func StartIPC(stack *node.Node, ctx *cli.Context) error {
var ethereum *eth.Ethereum
if err := stack.Service(&ethereum); err != nil {
return err
}
var shh *whisper.Whisper
if err := stack.Service(&shh); err != nil {
return err
}
config := comms.IpcConfig{
Endpoint: IpcSocketPath(ctx),
}
if ctx.GlobalIsSet(IPCExperimental.Name) {
listener, err := comms.CreateListener(config)
if err != nil {
return err
}
server := rpc.NewServer()
server.RegisterName("eth", accounts.NewAccountService(ethereum.AccountManager()))
server.RegisterName("eth", core.NewBlockChainService(ethereum.BlockChain(), ethereum.AccountManager()))
server.RegisterName("eth", core.NewTransactionPoolService(ethereum.TxPool(), ethereum.ChainDb(), ethereum.BlockChain(), ethereum.AccountManager()))
server.RegisterName("eth", miner.NewMinerService(ethereum.Miner()))
server.RegisterName("eth", eth.NewEthService(ethereum))
server.RegisterName("eth", downloader.NewDownloaderService(ethereum.Downloader()))
server.RegisterName("eth", filters.NewFilterService(ethereum.ChainDb(), ethereum.EventMux()))
server.RegisterName("net", p2p.NewNetService(stack.Server(), ethereum.NetVersion()))
server.RegisterName("web3", eth.NewWeb3Service(stack))
server.RegisterName("personal", accounts.NewPersonalService(ethereum.AccountManager()))
server.RegisterName("shh", whisper.NewWhisperService(shh))
go func() {
glog.Infof("IPC Service endpoint(%s)\n", config.Endpoint)
for {
conn, err := listener.Accept()
if err != nil {
glog.Errorf("%v\n", err)
continue
}
codec := rpc.NewJSONCodec(conn)
go server.ServeCodec(codec)
}
}()
return nil
}
initializer := func(conn net.Conn) (comms.Stopper, shared.EthereumApi, error) {
fe := useragent.NewRemoteFrontend(conn, ethereum.AccountManager())
xeth := xeth.New(stack, fe)
apis, err := api.ParseApiString(ctx.GlobalString(IPCApiFlag.Name), codec.JSON, xeth, stack)
if err != nil {
return nil, nil, err
}
return xeth, api.Merge(apis...), nil
}
return comms.StartIpc(config, codec.JSON, initializer)
}
// StartRPC starts a HTTP JSON-RPC API server. // StartRPC starts a HTTP JSON-RPC API server.
func StartRPC(stack *node.Node, ctx *cli.Context) error { func StartRPC(stack *node.Node, ctx *cli.Context) error {
config := comms.HttpConfig{ config := comms.HttpConfig{

View file

@ -87,10 +87,3 @@ func DefaultDataDir() string {
// As we cannot guess a stable location, return empty and handle later // As we cannot guess a stable location, return empty and handle later
return "" return ""
} }
func DefaultIpcPath() string {
if runtime.GOOS == "windows" {
return `\\.\pipe\geth.ipc`
}
return filepath.Join(DefaultDataDir(), "geth.ipc")
}

View file

@ -43,6 +43,7 @@ import (
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
const ( const (
@ -282,6 +283,12 @@ func (s *Ethereum) Protocols() []p2p.Protocol {
return s.protocolManager.SubProtocols return s.protocolManager.SubProtocols
} }
// Apis implements node.Servie, returning all the API handlers the service wants
// to expose.
func (s *Ethereum) Apis() (string, []rpc.Api) {
return "ethereum", []rpc.Api{}
}
// Start implements node.Service, starting all internal goroutines needed by the // Start implements node.Service, starting all internal goroutines needed by the
// Ethereum protocol implementation. // Ethereum protocol implementation.
func (s *Ethereum) Start(*p2p.Server) error { func (s *Ethereum) Start(*p2p.Server) error {

22
node/api.go Normal file
View file

@ -0,0 +1,22 @@
// 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 node
// PublicApi is the collection of public API methods exposed by the node.
type PublicApi struct {
node *Node
}

View file

@ -23,6 +23,8 @@ import (
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strings"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
@ -49,6 +51,12 @@ type Config struct {
// in memory. // in memory.
DataDir string DataDir string
// IpcPath is the requested location to place the IPC endpoint. If the path is
// a simple file name, it is placed inside the data directory (or on the root
// pipe path on Windows), whereas if it's a resolvable path name (absolute or
// relative), then that specific path is enforced. An empty path disables IPC.
IpcPath string
// This field should be a valid secp256k1 private key that will be used for both // This field should be a valid secp256k1 private key that will be used for both
// remote peer identification as well as network traffic encryption. If no key // remote peer identification as well as network traffic encryption. If no key
// is configured, the preset one is loaded from the data dir, generating it if // is configured, the preset one is loaded from the data dir, generating it if
@ -90,6 +98,31 @@ type Config struct {
MaxPendingPeers int MaxPendingPeers int
} }
// IpcEndpoint resolves an IPC endpoint based on a configured value, taking into
// account the set data folders as well as the designated platform we're currently
// running on.
func (c *Config) IpcEndpoint() string {
// Short circuit if IPC has not been enabled
if c.IpcPath == "" {
return ""
}
// On windows we can only use plain top-level pipes
if runtime.GOOS == "windows" {
if strings.HasPrefix(c.IpcPath, `\\.\pipe\`) {
return c.IpcPath
}
return `\\.\pipe\` + c.IpcPath
}
// Resolve names into the data directory full paths otherwise
if filepath.Base(c.IpcPath) == c.IpcPath {
if c.DataDir == "" {
return filepath.Join(os.TempDir(), c.IpcPath)
}
return filepath.Join(c.DataDir, c.IpcPath)
}
return c.IpcPath
}
// NodeKey retrieves the currently configured private key of the node, checking // NodeKey retrieves the currently configured private key of the node, checking
// first any manually set key, falling back to the one found in the configured // first any manually set key, falling back to the one found in the configured
// data folder. If no key can be found, a new one is generated. // data folder. If no key can be found, a new one is generated.

View file

@ -21,6 +21,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
@ -60,6 +61,37 @@ func TestDatadirCreation(t *testing.T) {
} }
} }
// Tests that IPC paths are correctly resolved to valid endpoints of different
// platforms.
func TestIpcPathResolution(t *testing.T) {
var tests = []struct {
DataDir string
IpcPath string
Windows bool
Endpoint string
}{
{"", "", false, ""},
{"data", "", false, ""},
{"", "geth.ipc", false, "/tmp/geth.ipc"},
{"data", "geth.ipc", false, "data/geth.ipc"},
{"data", "./geth.ipc", false, "./geth.ipc"},
{"data", "/geth.ipc", false, "/geth.ipc"},
{"", "", true, ``},
{"data", "", true, ``},
{"", "geth.ipc", true, `\\.\pipe\geth.ipc`},
{"data", "geth.ipc", true, `\\.\pipe\geth.ipc`},
{"data", `\\.\pipe\geth.ipc`, true, `\\.\pipe\geth.ipc`},
}
for i, test := range tests {
// Only run when platform/test match
if (runtime.GOOS == "windows") == test.Windows {
if endpoint := (&Config{DataDir: test.DataDir, IpcPath: test.IpcPath}).IpcEndpoint(); endpoint != test.Endpoint {
t.Errorf("test %d: IPC endpoint mismatch: have %s, want %s", i, endpoint, test.Endpoint)
}
}
}
}
// Tests that node keys can be correctly created, persisted, loaded and/or made // Tests that node keys can be correctly created, persisted, loaded and/or made
// ephemeral. // ephemeral.
func TestNodeKeyPersistency(t *testing.T) { func TestNodeKeyPersistency(t *testing.T) {

View file

@ -32,6 +32,17 @@ func (e *DuplicateServiceError) Error() string {
return fmt.Sprintf("duplicate service: %v", e.Kind) return fmt.Sprintf("duplicate service: %v", e.Kind)
} }
// DuplicateApiError is returned during Node startup if a registered service
// requests the registration of an already taken API endpoint.
type DuplicateApiError struct {
Namespace string
}
// Error generates a textual representation of the duplicate API error.
func (e *DuplicateApiError) Error() string {
return fmt.Sprintf("duplicate api endpoint: %s", e.Namespace)
}
// StopError is returned if a Node fails to stop either any of its registered // StopError is returned if a Node fails to stop either any of its registered
// services or itself. // services or itself.
type StopError struct { type StopError struct {

View file

@ -19,6 +19,8 @@ package node
import ( import (
"errors" "errors"
"fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -26,7 +28,11 @@ import (
"syscall" "syscall"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc/comms"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
var ( var (
@ -50,6 +56,9 @@ type Node struct {
serviceFuncs []ServiceConstructor // Service constructors (in dependency order) serviceFuncs []ServiceConstructor // Service constructors (in dependency order)
services map[reflect.Type]Service // Currently running services services map[reflect.Type]Service // Currently running services
ipcEndpoint string // IPC endpoint to listen at (empty = IPC disabled)
ipcListener net.Listener // IPC RPC listener socket to serve API requests
stop chan struct{} // Channel to wait for termination notifications stop chan struct{} // Channel to wait for termination notifications
lock sync.RWMutex lock sync.RWMutex
} }
@ -85,6 +94,7 @@ func New(conf *Config) (*Node, error) {
MaxPendingPeers: conf.MaxPendingPeers, MaxPendingPeers: conf.MaxPendingPeers,
}, },
serviceFuncs: []ServiceConstructor{}, serviceFuncs: []ServiceConstructor{},
ipcEndpoint: conf.IpcEndpoint(),
eventmux: new(event.TypeMux), eventmux: new(event.TypeMux),
}, nil }, nil
} }
@ -162,6 +172,14 @@ func (n *Node) Start() error {
// Mark the service started for potential cleanup // Mark the service started for potential cleanup
started = append(started, kind) started = append(started, kind)
} }
// Lastly start the configured RPC interfaces
if err := n.startRpc(services); err != nil {
for _, service := range services {
service.Stop()
}
running.Stop()
return err
}
// Finish initializing the startup // Finish initializing the startup
n.services = services n.services = services
n.server = running n.server = running
@ -170,6 +188,78 @@ func (n *Node) Start() error {
return nil return nil
} }
// startRpc initializes and starts the IPC and HTTP RPC endpoints.
func (n *Node) startRpc(services map[reflect.Type]Service) error {
// Gather and register all the APIs exposed by the services
ipcHandler := rpc.NewServer()
httpHandler := rpc.NewServer()
taken := make(map[string]struct{})
for _, service := range services {
name, apis := service.Apis()
for _, api := range apis {
// Ensure each API and version is unique
namespace := fmt.Sprintf("%s.v%d", name, api.Version)
if _, ok := taken[namespace]; ok {
return &DuplicateApiError{Namespace: namespace}
}
taken[namespace] = struct{}{}
// Namespace was free, register all the method handlers
for _, handler := range api.Handlers {
// Resolve the registration path
path := namespace
if handler.Path != "" {
path += "." + handler.Path
}
// Inject the methods into all API servers
if err := ipcHandler.RegisterName(path, handler.Handler); err != nil {
return err
}
if err := httpHandler.RegisterName(path, handler.Handler); err != nil {
return err
}
}
}
}
// All APIs registered, start the IPC and HTTP listeners
var (
ipcListener net.Listener
err error
)
if n.ipcEndpoint != "" {
if ipcListener, err = comms.CreateListener(comms.IpcConfig{Endpoint: n.ipcEndpoint}); err != nil {
return err
}
go func() {
glog.V(logger.Info).Infof("IPC endpoint opened: %s", n.ipcEndpoint)
defer glog.V(logger.Info).Infof("IPC endpoint closed: %s", n.ipcEndpoint)
for {
conn, err := ipcListener.Accept()
if err != nil {
// Terminate if the listener was closed
n.lock.RLock()
closed := n.ipcListener == nil
n.lock.RUnlock()
if closed {
return
}
// Not closed, just some error; report and continue
glog.V(logger.Error).Infof("IPC accept failed: %v", err)
continue
}
codec := rpc.NewJSONCodec(conn)
go ipcHandler.ServeCodec(codec)
}
}()
}
// All listeners booted successfully
n.ipcListener = ipcListener
return nil
}
// Stop terminates a running node along with all it's services. In the node was // Stop terminates a running node along with all it's services. In the node was
// not started, an error is returned. // not started, an error is returned.
func (n *Node) Stop() error { func (n *Node) Stop() error {
@ -180,7 +270,11 @@ func (n *Node) Stop() error {
if n.server == nil { if n.server == nil {
return ErrNodeStopped return ErrNodeStopped
} }
// Otherwise terminate all the services and the P2P server too // Otherwise terminate the API, all services and the P2P server too
if n.ipcListener != nil {
n.ipcListener.Close()
n.ipcListener = nil
}
failure := &StopError{ failure := &StopError{
Services: make(map[reflect.Type]error), Services: make(map[reflect.Type]error),
} }

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/discover" "github.com/ethereum/go-ethereum/p2p/discover"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
// SampleService is a trivial network service that can be attached to a node for // SampleService is a trivial network service that can be attached to a node for
@ -30,11 +31,13 @@ import (
// //
// The following methods are needed to implement a node.Service: // The following methods are needed to implement a node.Service:
// - Protocols() []p2p.Protocol - devp2p protocols the service can communicate on // - Protocols() []p2p.Protocol - devp2p protocols the service can communicate on
// - Apis() (string, []rpc.Api) - api methods the service wants to expose on rpc channels
// - Start() error - method invoked when the node is ready to start the service // - Start() error - method invoked when the node is ready to start the service
// - Stop() error - method invoked when the node terminates the service // - Stop() error - method invoked when the node terminates the service
type SampleService struct{} type SampleService struct{}
func (s *SampleService) Protocols() []p2p.Protocol { return nil } func (s *SampleService) Protocols() []p2p.Protocol { return nil }
func (s *SampleService) Apis() (string, []rpc.Api) { return "", nil }
func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil } func (s *SampleService) Start(*p2p.Server) error { fmt.Println("Service starting..."); return nil }
func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil } func (s *SampleService) Stop() error { fmt.Println("Service stopping..."); return nil }

View file

@ -18,19 +18,27 @@ package node
import ( import (
"errors" "errors"
"fmt"
"io/ioutil" "io/ioutil"
"math/rand"
"os" "os"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc/codec"
"github.com/ethereum/go-ethereum/rpc/comms"
"github.com/ethereum/go-ethereum/rpc/shared"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
var ( var (
testNodeKey, _ = crypto.GenerateKey() testNodeKey, _ = crypto.GenerateKey()
testNodeConfig = &Config{ testNodeConfig = &Config{
IpcPath: fmt.Sprintf("test-%d.ipc", rand.Int63()),
PrivateKey: testNodeKey, PrivateKey: testNodeKey,
Name: "test node", Name: "test node",
} }
@ -494,3 +502,81 @@ func TestProtocolGather(t *testing.T) {
} }
} }
} }
// Tests that all APIs defined by individual services get exposed.
func TestApiGather(t *testing.T) {
stack, err := New(testNodeConfig)
if err != nil {
t.Fatalf("failed to create protocol stack: %v", err)
}
// Register a batch of services with some configured APIs
calls := make(chan string, 1)
services := map[string]struct {
Namespace string
Apis []rpc.Api
Maker InstrumentingWrapper
}{
"Zero APIs": {"", []rpc.Api{}, InstrumentedServiceMakerA},
"Single API": {"single", []rpc.Api{
{1, []rpc.ApiHandler{{Handler: &OneMethodApi{fun: func() { calls <- "single.v1" }}}}},
}, InstrumentedServiceMakerB},
"Many APIs": {"multi", []rpc.Api{
{1, []rpc.ApiHandler{{Handler: &OneMethodApi{fun: func() { calls <- "multi.v1" }}}}},
{2, []rpc.ApiHandler{
{Handler: &OneMethodApi{fun: func() { calls <- "multi.v2" }}},
{Path: "nested", Handler: &OneMethodApi{fun: func() { calls <- "multi.v2.nested" }}},
}},
}, InstrumentedServiceMakerC},
}
for id, config := range services {
config := config
constructor := func(*ServiceContext) (Service, error) {
return &InstrumentedService{
apiNamespace: config.Namespace,
apiHandlers: config.Apis,
}, nil
}
if err := stack.Register(config.Maker(constructor)); err != nil {
t.Fatalf("service %s: registration failed: %v", id, err)
}
}
// Start the services and ensure all API start successfully
if err := stack.Start(); err != nil {
t.Fatalf("failed to start protocol stack: %v", err)
}
defer stack.Stop()
// Connect to the RPC server and verify the various registered endpoints
ipcClient, err := comms.NewIpcClient(comms.IpcConfig{Endpoint: testNodeConfig.IpcEndpoint()}, codec.JSON)
if err != nil {
t.Fatalf("failed to connect to the IPC API server: %v", err)
}
tests := []struct {
Method string
Result string
}{
{"single.v1.theOneMethod", "single.v1"},
{"multi.v1.theOneMethod", "multi.v1"},
{"multi.v2.theOneMethod", "multi.v2"},
{"multi.v2.nested.theOneMethod", "multi.v2.nested"},
}
for i, test := range tests {
if err := ipcClient.Send(shared.Request{Id: i, Jsonrpc: "2.0", Method: test.Method}); err != nil {
t.Fatalf("test %d: failed to send API request: %v", i, err)
}
_, err := ipcClient.Recv()
if err != nil {
t.Fatalf("test %d: failed to read API reply: %v", i, err)
}
select {
case result := <-calls:
if result != test.Result {
t.Errorf("test %d: result mismatch: have %s, want %s", i, result, test.Result)
}
case <-time.After(time.Second):
t.Fatalf("test %d: rpc execution timeout", i)
}
}
}

View file

@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
// ServiceContext is a collection of service independent options inherited from // ServiceContext is a collection of service independent options inherited from
@ -70,6 +71,10 @@ type Service interface {
// Protocol retrieves the P2P protocols the service wishes to start. // Protocol retrieves the P2P protocols the service wishes to start.
Protocols() []p2p.Protocol Protocols() []p2p.Protocol
// Apis retrieves the list of versioned APIs the service provides to register
// under the requested top level API namespace.
Apis() (string, []rpc.Api)
// Start is called after all services have been constructed and the networking // Start is called after all services have been constructed and the networking
// layer was also initialized to spawn any goroutines required by the service. // layer was also initialized to spawn any goroutines required by the service.
Start(server *p2p.Server) error Start(server *p2p.Server) error

View file

@ -23,12 +23,14 @@ import (
"reflect" "reflect"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
) )
// NoopService is a trivial implementation of the Service interface. // NoopService is a trivial implementation of the Service interface.
type NoopService struct{} type NoopService struct{}
func (s *NoopService) Protocols() []p2p.Protocol { return nil } func (s *NoopService) Protocols() []p2p.Protocol { return nil }
func (s *NoopService) Apis() (string, []rpc.Api) { return "", nil }
func (s *NoopService) Start(*p2p.Server) error { return nil } func (s *NoopService) Start(*p2p.Server) error { return nil }
func (s *NoopService) Stop() error { return nil } func (s *NoopService) Stop() error { return nil }
@ -50,10 +52,13 @@ func NewNoopServiceD(*ServiceContext) (Service, error) { return new(NoopServiceD
// methods can be instrumented both return value as well as event hook wise. // methods can be instrumented both return value as well as event hook wise.
type InstrumentedService struct { type InstrumentedService struct {
protocols []p2p.Protocol protocols []p2p.Protocol
apiNamespace string
apiHandlers []rpc.Api
start error start error
stop error stop error
protocolsHook func() protocolsHook func()
apisHook func()
startHook func(*p2p.Server) startHook func(*p2p.Server)
stopHook func() stopHook func()
} }
@ -67,6 +72,13 @@ func (s *InstrumentedService) Protocols() []p2p.Protocol {
return s.protocols return s.protocols
} }
func (s *InstrumentedService) Apis() (string, []rpc.Api) {
if s.apisHook != nil {
s.apisHook()
}
return s.apiNamespace, s.apiHandlers
}
func (s *InstrumentedService) Start(server *p2p.Server) error { func (s *InstrumentedService) Start(server *p2p.Server) error {
if s.startHook != nil { if s.startHook != nil {
s.startHook(server) s.startHook(server)
@ -115,3 +127,14 @@ func InstrumentedServiceMakerB(base ServiceConstructor) ServiceConstructor {
func InstrumentedServiceMakerC(base ServiceConstructor) ServiceConstructor { func InstrumentedServiceMakerC(base ServiceConstructor) ServiceConstructor {
return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceC{})) return InstrumentingWrapperMaker(base, reflect.TypeOf(InstrumentedServiceC{}))
} }
// OneMethodApi is a single-method API handler to be returned by test services.
type OneMethodApi struct {
fun func()
}
func (api *OneMethodApi) TheOneMethod() {
if api.fun != nil {
api.fun()
}
}

30
rpc/v2/api.go Normal file
View file

@ -0,0 +1,30 @@
// 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 v2
// Api describes a versioned API schema offered over the RPC interface.
type Api struct {
Version int // Version number under which to advertise this method set
Handlers []ApiHandler // List of API handlers advertized on this API version
}
// ApiHandler describes the set of methods offered over the RPC interface
type ApiHandler struct {
Path string // Path under which the RPC methods of Handler are to be exposed
Handler interface{} // Receiver instance which holds the methods to expose
Public bool // Indication if the methods can be considered safe for public use
}

View file

@ -190,7 +190,6 @@ METHODS:
default: default:
continue METHODS continue METHODS
} }
callbacks[mname] = &h callbacks[mname] = &h
} }

View file

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/logger/glog"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
rpc "github.com/ethereum/go-ethereum/rpc/v2"
"gopkg.in/fatih/set.v0" "gopkg.in/fatih/set.v0"
) )
@ -103,6 +104,12 @@ func (self *Whisper) Protocols() []p2p.Protocol {
return []p2p.Protocol{self.protocol} return []p2p.Protocol{self.protocol}
} }
// Apis implements node.Servie, returning all the API handlers the service wants
// to expose.
func (self *Whisper) Apis() (string, []rpc.Api) {
return "whisper", []rpc.Api{}
}
// Version returns the whisper sub-protocols version number. // Version returns the whisper sub-protocols version number.
func (self *Whisper) Version() uint { func (self *Whisper) Version() uint {
return self.protocol.Version return self.protocol.Version