github.com/sberex/go-sberex@v1.8.2-0.20181113200658-ed96ac38f7d7/p2p/simulations/adapters/exec.go (about)

     1  // This file is part of the go-sberex library. The go-sberex library is 
     2  // free software: you can redistribute it and/or modify it under the terms 
     3  // of the GNU Lesser General Public License as published by the Free 
     4  // Software Foundation, either version 3 of the License, or (at your option)
     5  // any later version.
     6  //
     7  // The go-sberex library is distributed in the hope that it will be useful, 
     8  // but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser 
    10  // General Public License <http://www.gnu.org/licenses/> for more details.
    11  
    12  package adapters
    13  
    14  import (
    15  	"bufio"
    16  	"context"
    17  	"crypto/ecdsa"
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net"
    23  	"os"
    24  	"os/exec"
    25  	"os/signal"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strings"
    29  	"sync"
    30  	"syscall"
    31  	"time"
    32  
    33  	"github.com/docker/docker/pkg/reexec"
    34  	"github.com/Sberex/go-sberex/log"
    35  	"github.com/Sberex/go-sberex/node"
    36  	"github.com/Sberex/go-sberex/p2p"
    37  	"github.com/Sberex/go-sberex/p2p/discover"
    38  	"github.com/Sberex/go-sberex/rpc"
    39  	"golang.org/x/net/websocket"
    40  )
    41  
    42  // ExecAdapter is a NodeAdapter which runs simulation nodes by executing the
    43  // current binary as a child process.
    44  //
    45  // An init hook is used so that the child process executes the node services
    46  // (rather than whataver the main() function would normally do), see the
    47  // execP2PNode function for more information.
    48  type ExecAdapter struct {
    49  	// BaseDir is the directory under which the data directories for each
    50  	// simulation node are created.
    51  	BaseDir string
    52  
    53  	nodes map[discover.NodeID]*ExecNode
    54  }
    55  
    56  // NewExecAdapter returns an ExecAdapter which stores node data in
    57  // subdirectories of the given base directory
    58  func NewExecAdapter(baseDir string) *ExecAdapter {
    59  	return &ExecAdapter{
    60  		BaseDir: baseDir,
    61  		nodes:   make(map[discover.NodeID]*ExecNode),
    62  	}
    63  }
    64  
    65  // Name returns the name of the adapter for logging purposes
    66  func (e *ExecAdapter) Name() string {
    67  	return "exec-adapter"
    68  }
    69  
    70  // NewNode returns a new ExecNode using the given config
    71  func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) {
    72  	if len(config.Services) == 0 {
    73  		return nil, errors.New("node must have at least one service")
    74  	}
    75  	for _, service := range config.Services {
    76  		if _, exists := serviceFuncs[service]; !exists {
    77  			return nil, fmt.Errorf("unknown node service %q", service)
    78  		}
    79  	}
    80  
    81  	// create the node directory using the first 12 characters of the ID
    82  	// as Unix socket paths cannot be longer than 256 characters
    83  	dir := filepath.Join(e.BaseDir, config.ID.String()[:12])
    84  	if err := os.Mkdir(dir, 0755); err != nil {
    85  		return nil, fmt.Errorf("error creating node directory: %s", err)
    86  	}
    87  
    88  	// generate the config
    89  	conf := &execNodeConfig{
    90  		Stack: node.DefaultConfig,
    91  		Node:  config,
    92  	}
    93  	conf.Stack.DataDir = filepath.Join(dir, "data")
    94  	conf.Stack.WSHost = "127.0.0.1"
    95  	conf.Stack.WSPort = 0
    96  	conf.Stack.WSOrigins = []string{"*"}
    97  	conf.Stack.WSExposeAll = true
    98  	conf.Stack.P2P.EnableMsgEvents = false
    99  	conf.Stack.P2P.NoDiscovery = true
   100  	conf.Stack.P2P.NAT = nil
   101  	conf.Stack.NoUSB = true
   102  
   103  	// listen on a random localhost port (we'll get the actual port after
   104  	// starting the node through the RPC admin.nodeInfo method)
   105  	conf.Stack.P2P.ListenAddr = "127.0.0.1:0"
   106  
   107  	node := &ExecNode{
   108  		ID:      config.ID,
   109  		Dir:     dir,
   110  		Config:  conf,
   111  		adapter: e,
   112  	}
   113  	node.newCmd = node.execCommand
   114  	e.nodes[node.ID] = node
   115  	return node, nil
   116  }
   117  
   118  // ExecNode starts a simulation node by exec'ing the current binary and
   119  // running the configured services
   120  type ExecNode struct {
   121  	ID     discover.NodeID
   122  	Dir    string
   123  	Config *execNodeConfig
   124  	Cmd    *exec.Cmd
   125  	Info   *p2p.NodeInfo
   126  
   127  	adapter *ExecAdapter
   128  	client  *rpc.Client
   129  	wsAddr  string
   130  	newCmd  func() *exec.Cmd
   131  	key     *ecdsa.PrivateKey
   132  }
   133  
   134  // Addr returns the node's enode URL
   135  func (n *ExecNode) Addr() []byte {
   136  	if n.Info == nil {
   137  		return nil
   138  	}
   139  	return []byte(n.Info.Enode)
   140  }
   141  
   142  // Client returns an rpc.Client which can be used to communicate with the
   143  // underlying services (it is set once the node has started)
   144  func (n *ExecNode) Client() (*rpc.Client, error) {
   145  	return n.client, nil
   146  }
   147  
   148  // wsAddrPattern is a regex used to read the WebSocket address from the node's
   149  // log
   150  var wsAddrPattern = regexp.MustCompile(`ws://[\d.:]+`)
   151  
   152  // Start exec's the node passing the ID and service as command line arguments
   153  // and the node config encoded as JSON in the _P2P_NODE_CONFIG environment
   154  // variable
   155  func (n *ExecNode) Start(snapshots map[string][]byte) (err error) {
   156  	if n.Cmd != nil {
   157  		return errors.New("already started")
   158  	}
   159  	defer func() {
   160  		if err != nil {
   161  			log.Error("node failed to start", "err", err)
   162  			n.Stop()
   163  		}
   164  	}()
   165  
   166  	// encode a copy of the config containing the snapshot
   167  	confCopy := *n.Config
   168  	confCopy.Snapshots = snapshots
   169  	confCopy.PeerAddrs = make(map[string]string)
   170  	for id, node := range n.adapter.nodes {
   171  		confCopy.PeerAddrs[id.String()] = node.wsAddr
   172  	}
   173  	confData, err := json.Marshal(confCopy)
   174  	if err != nil {
   175  		return fmt.Errorf("error generating node config: %s", err)
   176  	}
   177  
   178  	// use a pipe for stderr so we can both copy the node's stderr to
   179  	// os.Stderr and read the WebSocket address from the logs
   180  	stderrR, stderrW := io.Pipe()
   181  	stderr := io.MultiWriter(os.Stderr, stderrW)
   182  
   183  	// start the node
   184  	cmd := n.newCmd()
   185  	cmd.Stdout = os.Stdout
   186  	cmd.Stderr = stderr
   187  	cmd.Env = append(os.Environ(), fmt.Sprintf("_P2P_NODE_CONFIG=%s", confData))
   188  	if err := cmd.Start(); err != nil {
   189  		return fmt.Errorf("error starting node: %s", err)
   190  	}
   191  	n.Cmd = cmd
   192  
   193  	// read the WebSocket address from the stderr logs
   194  	var wsAddr string
   195  	wsAddrC := make(chan string)
   196  	go func() {
   197  		s := bufio.NewScanner(stderrR)
   198  		for s.Scan() {
   199  			if strings.Contains(s.Text(), "WebSocket endpoint opened:") {
   200  				wsAddrC <- wsAddrPattern.FindString(s.Text())
   201  			}
   202  		}
   203  	}()
   204  	select {
   205  	case wsAddr = <-wsAddrC:
   206  		if wsAddr == "" {
   207  			return errors.New("failed to read WebSocket address from stderr")
   208  		}
   209  	case <-time.After(10 * time.Second):
   210  		return errors.New("timed out waiting for WebSocket address on stderr")
   211  	}
   212  
   213  	// create the RPC client and load the node info
   214  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   215  	defer cancel()
   216  	client, err := rpc.DialWebsocket(ctx, wsAddr, "")
   217  	if err != nil {
   218  		return fmt.Errorf("error dialing rpc websocket: %s", err)
   219  	}
   220  	var info p2p.NodeInfo
   221  	if err := client.CallContext(ctx, &info, "admin_nodeInfo"); err != nil {
   222  		return fmt.Errorf("error getting node info: %s", err)
   223  	}
   224  	n.client = client
   225  	n.wsAddr = wsAddr
   226  	n.Info = &info
   227  
   228  	return nil
   229  }
   230  
   231  // execCommand returns a command which runs the node locally by exec'ing
   232  // the current binary but setting argv[0] to "p2p-node" so that the child
   233  // runs execP2PNode
   234  func (n *ExecNode) execCommand() *exec.Cmd {
   235  	return &exec.Cmd{
   236  		Path: reexec.Self(),
   237  		Args: []string{"p2p-node", strings.Join(n.Config.Node.Services, ","), n.ID.String()},
   238  	}
   239  }
   240  
   241  // Stop stops the node by first sending SIGTERM and then SIGKILL if the node
   242  // doesn't stop within 5s
   243  func (n *ExecNode) Stop() error {
   244  	if n.Cmd == nil {
   245  		return nil
   246  	}
   247  	defer func() {
   248  		n.Cmd = nil
   249  	}()
   250  
   251  	if n.client != nil {
   252  		n.client.Close()
   253  		n.client = nil
   254  		n.wsAddr = ""
   255  		n.Info = nil
   256  	}
   257  
   258  	if err := n.Cmd.Process.Signal(syscall.SIGTERM); err != nil {
   259  		return n.Cmd.Process.Kill()
   260  	}
   261  	waitErr := make(chan error)
   262  	go func() {
   263  		waitErr <- n.Cmd.Wait()
   264  	}()
   265  	select {
   266  	case err := <-waitErr:
   267  		return err
   268  	case <-time.After(5 * time.Second):
   269  		return n.Cmd.Process.Kill()
   270  	}
   271  }
   272  
   273  // NodeInfo returns information about the node
   274  func (n *ExecNode) NodeInfo() *p2p.NodeInfo {
   275  	info := &p2p.NodeInfo{
   276  		ID: n.ID.String(),
   277  	}
   278  	if n.client != nil {
   279  		n.client.Call(&info, "admin_nodeInfo")
   280  	}
   281  	return info
   282  }
   283  
   284  // ServeRPC serves RPC requests over the given connection by dialling the
   285  // node's WebSocket address and joining the two connections
   286  func (n *ExecNode) ServeRPC(clientConn net.Conn) error {
   287  	conn, err := websocket.Dial(n.wsAddr, "", "http://localhost")
   288  	if err != nil {
   289  		return err
   290  	}
   291  	var wg sync.WaitGroup
   292  	wg.Add(2)
   293  	join := func(src, dst net.Conn) {
   294  		defer wg.Done()
   295  		io.Copy(dst, src)
   296  		// close the write end of the destination connection
   297  		if cw, ok := dst.(interface {
   298  			CloseWrite() error
   299  		}); ok {
   300  			cw.CloseWrite()
   301  		} else {
   302  			dst.Close()
   303  		}
   304  	}
   305  	go join(conn, clientConn)
   306  	go join(clientConn, conn)
   307  	wg.Wait()
   308  	return nil
   309  }
   310  
   311  // Snapshots creates snapshots of the services by calling the
   312  // simulation_snapshot RPC method
   313  func (n *ExecNode) Snapshots() (map[string][]byte, error) {
   314  	if n.client == nil {
   315  		return nil, errors.New("RPC not started")
   316  	}
   317  	var snapshots map[string][]byte
   318  	return snapshots, n.client.Call(&snapshots, "simulation_snapshot")
   319  }
   320  
   321  func init() {
   322  	// register a reexec function to start a devp2p node when the current
   323  	// binary is executed as "p2p-node"
   324  	reexec.Register("p2p-node", execP2PNode)
   325  }
   326  
   327  // execNodeConfig is used to serialize the node configuration so it can be
   328  // passed to the child process as a JSON encoded environment variable
   329  type execNodeConfig struct {
   330  	Stack     node.Config       `json:"stack"`
   331  	Node      *NodeConfig       `json:"node"`
   332  	Snapshots map[string][]byte `json:"snapshots,omitempty"`
   333  	PeerAddrs map[string]string `json:"peer_addrs,omitempty"`
   334  }
   335  
   336  // execP2PNode starts a devp2p node when the current binary is executed with
   337  // argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2]
   338  // and the node config from the _P2P_NODE_CONFIG environment variable
   339  func execP2PNode() {
   340  	glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat()))
   341  	glogger.Verbosity(log.LvlInfo)
   342  	log.Root().SetHandler(glogger)
   343  
   344  	// read the services from argv
   345  	serviceNames := strings.Split(os.Args[1], ",")
   346  
   347  	// decode the config
   348  	confEnv := os.Getenv("_P2P_NODE_CONFIG")
   349  	if confEnv == "" {
   350  		log.Crit("missing _P2P_NODE_CONFIG")
   351  	}
   352  	var conf execNodeConfig
   353  	if err := json.Unmarshal([]byte(confEnv), &conf); err != nil {
   354  		log.Crit("error decoding _P2P_NODE_CONFIG", "err", err)
   355  	}
   356  	conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey
   357  	conf.Stack.Logger = log.New("node.id", conf.Node.ID.String())
   358  
   359  	// use explicit IP address in ListenAddr so that Enode URL is usable
   360  	externalIP := func() string {
   361  		addrs, err := net.InterfaceAddrs()
   362  		if err != nil {
   363  			log.Crit("error getting IP address", "err", err)
   364  		}
   365  		for _, addr := range addrs {
   366  			if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() {
   367  				return ip.IP.String()
   368  			}
   369  		}
   370  		log.Crit("unable to determine explicit IP address")
   371  		return ""
   372  	}
   373  	if strings.HasPrefix(conf.Stack.P2P.ListenAddr, ":") {
   374  		conf.Stack.P2P.ListenAddr = externalIP() + conf.Stack.P2P.ListenAddr
   375  	}
   376  	if conf.Stack.WSHost == "0.0.0.0" {
   377  		conf.Stack.WSHost = externalIP()
   378  	}
   379  
   380  	// initialize the devp2p stack
   381  	stack, err := node.New(&conf.Stack)
   382  	if err != nil {
   383  		log.Crit("error creating node stack", "err", err)
   384  	}
   385  
   386  	// register the services, collecting them into a map so we can wrap
   387  	// them in a snapshot service
   388  	services := make(map[string]node.Service, len(serviceNames))
   389  	for _, name := range serviceNames {
   390  		serviceFunc, exists := serviceFuncs[name]
   391  		if !exists {
   392  			log.Crit("unknown node service", "name", name)
   393  		}
   394  		constructor := func(nodeCtx *node.ServiceContext) (node.Service, error) {
   395  			ctx := &ServiceContext{
   396  				RPCDialer:   &wsRPCDialer{addrs: conf.PeerAddrs},
   397  				NodeContext: nodeCtx,
   398  				Config:      conf.Node,
   399  			}
   400  			if conf.Snapshots != nil {
   401  				ctx.Snapshot = conf.Snapshots[name]
   402  			}
   403  			service, err := serviceFunc(ctx)
   404  			if err != nil {
   405  				return nil, err
   406  			}
   407  			services[name] = service
   408  			return service, nil
   409  		}
   410  		if err := stack.Register(constructor); err != nil {
   411  			log.Crit("error starting service", "name", name, "err", err)
   412  		}
   413  	}
   414  
   415  	// register the snapshot service
   416  	if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
   417  		return &snapshotService{services}, nil
   418  	}); err != nil {
   419  		log.Crit("error starting snapshot service", "err", err)
   420  	}
   421  
   422  	// start the stack
   423  	if err := stack.Start(); err != nil {
   424  		log.Crit("error stating node stack", "err", err)
   425  	}
   426  
   427  	// stop the stack if we get a SIGTERM signal
   428  	go func() {
   429  		sigc := make(chan os.Signal, 1)
   430  		signal.Notify(sigc, syscall.SIGTERM)
   431  		defer signal.Stop(sigc)
   432  		<-sigc
   433  		log.Info("Received SIGTERM, shutting down...")
   434  		stack.Stop()
   435  	}()
   436  
   437  	// wait for the stack to exit
   438  	stack.Wait()
   439  }
   440  
   441  // snapshotService is a node.Service which wraps a list of services and
   442  // exposes an API to generate a snapshot of those services
   443  type snapshotService struct {
   444  	services map[string]node.Service
   445  }
   446  
   447  func (s *snapshotService) APIs() []rpc.API {
   448  	return []rpc.API{{
   449  		Namespace: "simulation",
   450  		Version:   "1.0",
   451  		Service:   SnapshotAPI{s.services},
   452  	}}
   453  }
   454  
   455  func (s *snapshotService) Protocols() []p2p.Protocol {
   456  	return nil
   457  }
   458  
   459  func (s *snapshotService) Start(*p2p.Server) error {
   460  	return nil
   461  }
   462  
   463  func (s *snapshotService) Stop() error {
   464  	return nil
   465  }
   466  
   467  // SnapshotAPI provides an RPC method to create snapshots of services
   468  type SnapshotAPI struct {
   469  	services map[string]node.Service
   470  }
   471  
   472  func (api SnapshotAPI) Snapshot() (map[string][]byte, error) {
   473  	snapshots := make(map[string][]byte)
   474  	for name, service := range api.services {
   475  		if s, ok := service.(interface {
   476  			Snapshot() ([]byte, error)
   477  		}); ok {
   478  			snap, err := s.Snapshot()
   479  			if err != nil {
   480  				return nil, err
   481  			}
   482  			snapshots[name] = snap
   483  		}
   484  	}
   485  	return snapshots, nil
   486  }
   487  
   488  type wsRPCDialer struct {
   489  	addrs map[string]string
   490  }
   491  
   492  // DialRPC implements the RPCDialer interface by creating a WebSocket RPC
   493  // client of the given node
   494  func (w *wsRPCDialer) DialRPC(id discover.NodeID) (*rpc.Client, error) {
   495  	addr, ok := w.addrs[id.String()]
   496  	if !ok {
   497  		return nil, fmt.Errorf("unknown node: %s", id)
   498  	}
   499  	return rpc.DialWebsocket(context.Background(), addr, "http://localhost")
   500  }