github.com/luckypickle/go-ethereum-vet@v1.14.2/p2p/simulations/adapters/exec.go (about) 1 // Copyright 2017 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package adapters 18 19 import ( 20 "bufio" 21 "context" 22 "crypto/ecdsa" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "net" 28 "os" 29 "os/exec" 30 "os/signal" 31 "path/filepath" 32 "strings" 33 "sync" 34 "syscall" 35 "time" 36 37 "github.com/docker/docker/pkg/reexec" 38 "github.com/luckypickle/go-ethereum-vet/log" 39 "github.com/luckypickle/go-ethereum-vet/node" 40 "github.com/luckypickle/go-ethereum-vet/p2p" 41 "github.com/luckypickle/go-ethereum-vet/p2p/discover" 42 "github.com/luckypickle/go-ethereum-vet/rpc" 43 "golang.org/x/net/websocket" 44 ) 45 46 // ExecAdapter is a NodeAdapter which runs simulation nodes by executing the 47 // current binary as a child process. 48 // 49 // An init hook is used so that the child process executes the node services 50 // (rather than whataver the main() function would normally do), see the 51 // execP2PNode function for more information. 52 type ExecAdapter struct { 53 // BaseDir is the directory under which the data directories for each 54 // simulation node are created. 55 BaseDir string 56 57 nodes map[discover.NodeID]*ExecNode 58 } 59 60 // NewExecAdapter returns an ExecAdapter which stores node data in 61 // subdirectories of the given base directory 62 func NewExecAdapter(baseDir string) *ExecAdapter { 63 return &ExecAdapter{ 64 BaseDir: baseDir, 65 nodes: make(map[discover.NodeID]*ExecNode), 66 } 67 } 68 69 // Name returns the name of the adapter for logging purposes 70 func (e *ExecAdapter) Name() string { 71 return "exec-adapter" 72 } 73 74 // NewNode returns a new ExecNode using the given config 75 func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) { 76 if len(config.Services) == 0 { 77 return nil, errors.New("node must have at least one service") 78 } 79 for _, service := range config.Services { 80 if _, exists := serviceFuncs[service]; !exists { 81 return nil, fmt.Errorf("unknown node service %q", service) 82 } 83 } 84 85 // create the node directory using the first 12 characters of the ID 86 // as Unix socket paths cannot be longer than 256 characters 87 dir := filepath.Join(e.BaseDir, config.ID.String()[:12]) 88 if err := os.Mkdir(dir, 0755); err != nil { 89 return nil, fmt.Errorf("error creating node directory: %s", err) 90 } 91 92 // generate the config 93 conf := &execNodeConfig{ 94 Stack: node.DefaultConfig, 95 Node: config, 96 } 97 conf.Stack.DataDir = filepath.Join(dir, "data") 98 conf.Stack.WSHost = "127.0.0.1" 99 conf.Stack.WSPort = 0 100 conf.Stack.WSOrigins = []string{"*"} 101 conf.Stack.WSExposeAll = true 102 conf.Stack.P2P.EnableMsgEvents = false 103 conf.Stack.P2P.NoDiscovery = true 104 conf.Stack.P2P.NAT = nil 105 conf.Stack.NoUSB = true 106 107 // listen on a localhost port, which we set when we 108 // initialise NodeConfig (usually a random port) 109 conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port) 110 111 node := &ExecNode{ 112 ID: config.ID, 113 Dir: dir, 114 Config: conf, 115 adapter: e, 116 } 117 node.newCmd = node.execCommand 118 e.nodes[node.ID] = node 119 return node, nil 120 } 121 122 // ExecNode starts a simulation node by exec'ing the current binary and 123 // running the configured services 124 type ExecNode struct { 125 ID discover.NodeID 126 Dir string 127 Config *execNodeConfig 128 Cmd *exec.Cmd 129 Info *p2p.NodeInfo 130 131 adapter *ExecAdapter 132 client *rpc.Client 133 wsAddr string 134 newCmd func() *exec.Cmd 135 key *ecdsa.PrivateKey 136 } 137 138 // Addr returns the node's enode URL 139 func (n *ExecNode) Addr() []byte { 140 if n.Info == nil { 141 return nil 142 } 143 return []byte(n.Info.Enode) 144 } 145 146 // Client returns an rpc.Client which can be used to communicate with the 147 // underlying services (it is set once the node has started) 148 func (n *ExecNode) Client() (*rpc.Client, error) { 149 return n.client, nil 150 } 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 // ExternalIP gets an external IP address so that Enode URL is usable 337 func ExternalIP() net.IP { 338 addrs, err := net.InterfaceAddrs() 339 if err != nil { 340 log.Crit("error getting IP address", "err", err) 341 } 342 for _, addr := range addrs { 343 if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && !ip.IP.IsLinkLocalUnicast() { 344 return ip.IP 345 } 346 } 347 log.Warn("unable to determine explicit IP address, falling back to loopback") 348 return net.IP{127, 0, 0, 1} 349 } 350 351 // execP2PNode starts a devp2p node when the current binary is executed with 352 // argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2] 353 // and the node config from the _P2P_NODE_CONFIG environment variable 354 func execP2PNode() { 355 glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat())) 356 glogger.Verbosity(log.LvlInfo) 357 log.Root().SetHandler(glogger) 358 359 // read the services from argv 360 serviceNames := strings.Split(os.Args[1], ",") 361 362 // decode the config 363 confEnv := os.Getenv("_P2P_NODE_CONFIG") 364 if confEnv == "" { 365 log.Crit("missing _P2P_NODE_CONFIG") 366 } 367 var conf execNodeConfig 368 if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { 369 log.Crit("error decoding _P2P_NODE_CONFIG", "err", err) 370 } 371 conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey 372 conf.Stack.Logger = log.New("node.id", conf.Node.ID.String()) 373 374 if strings.HasPrefix(conf.Stack.P2P.ListenAddr, ":") { 375 conf.Stack.P2P.ListenAddr = ExternalIP().String() + conf.Stack.P2P.ListenAddr 376 } 377 if conf.Stack.WSHost == "0.0.0.0" { 378 conf.Stack.WSHost = ExternalIP().String() 379 } 380 381 // initialize the devp2p stack 382 stack, err := node.New(&conf.Stack) 383 if err != nil { 384 log.Crit("error creating node stack", "err", err) 385 } 386 387 // register the services, collecting them into a map so we can wrap 388 // them in a snapshot service 389 services := make(map[string]node.Service, len(serviceNames)) 390 for _, name := range serviceNames { 391 serviceFunc, exists := serviceFuncs[name] 392 if !exists { 393 log.Crit("unknown node service", "name", name) 394 } 395 constructor := func(nodeCtx *node.ServiceContext) (node.Service, error) { 396 ctx := &ServiceContext{ 397 RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs}, 398 NodeContext: nodeCtx, 399 Config: conf.Node, 400 } 401 if conf.Snapshots != nil { 402 ctx.Snapshot = conf.Snapshots[name] 403 } 404 service, err := serviceFunc(ctx) 405 if err != nil { 406 return nil, err 407 } 408 services[name] = service 409 return service, nil 410 } 411 if err := stack.Register(constructor); err != nil { 412 log.Crit("error starting service", "name", name, "err", err) 413 } 414 } 415 416 // register the snapshot service 417 if err := stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { 418 return &snapshotService{services}, nil 419 }); err != nil { 420 log.Crit("error starting snapshot service", "err", err) 421 } 422 423 // start the stack 424 if err := stack.Start(); err != nil { 425 log.Crit("error stating node stack", "err", err) 426 } 427 428 // stop the stack if we get a SIGTERM signal 429 go func() { 430 sigc := make(chan os.Signal, 1) 431 signal.Notify(sigc, syscall.SIGTERM) 432 defer signal.Stop(sigc) 433 <-sigc 434 log.Info("Received SIGTERM, shutting down...") 435 stack.Stop() 436 }() 437 438 // wait for the stack to exit 439 stack.Wait() 440 } 441 442 // snapshotService is a node.Service which wraps a list of services and 443 // exposes an API to generate a snapshot of those services 444 type snapshotService struct { 445 services map[string]node.Service 446 } 447 448 func (s *snapshotService) APIs() []rpc.API { 449 return []rpc.API{{ 450 Namespace: "simulation", 451 Version: "1.0", 452 Service: SnapshotAPI{s.services}, 453 }} 454 } 455 456 func (s *snapshotService) Protocols() []p2p.Protocol { 457 return nil 458 } 459 460 func (s *snapshotService) Start(*p2p.Server) error { 461 return nil 462 } 463 464 func (s *snapshotService) Stop() error { 465 return nil 466 } 467 468 // SnapshotAPI provides an RPC method to create snapshots of services 469 type SnapshotAPI struct { 470 services map[string]node.Service 471 } 472 473 func (api SnapshotAPI) Snapshot() (map[string][]byte, error) { 474 snapshots := make(map[string][]byte) 475 for name, service := range api.services { 476 if s, ok := service.(interface { 477 Snapshot() ([]byte, error) 478 }); ok { 479 snap, err := s.Snapshot() 480 if err != nil { 481 return nil, err 482 } 483 snapshots[name] = snap 484 } 485 } 486 return snapshots, nil 487 } 488 489 type wsRPCDialer struct { 490 addrs map[string]string 491 } 492 493 // DialRPC implements the RPCDialer interface by creating a WebSocket RPC 494 // client of the given node 495 func (w *wsRPCDialer) DialRPC(id discover.NodeID) (*rpc.Client, error) { 496 addr, ok := w.addrs[id.String()] 497 if !ok { 498 return nil, fmt.Errorf("unknown node: %s", id) 499 } 500 return rpc.DialWebsocket(context.Background(), addr, "http://localhost") 501 }