github.com/codysnider/go-ethereum@v1.10.18-0.20220420071915-14f4ae99222a/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 "bytes" 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 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/ethereum/go-ethereum/log" 39 "github.com/ethereum/go-ethereum/node" 40 "github.com/ethereum/go-ethereum/p2p" 41 "github.com/ethereum/go-ethereum/p2p/enode" 42 "github.com/ethereum/go-ethereum/rpc" 43 "github.com/gorilla/websocket" 44 ) 45 46 func init() { 47 // Register a reexec function to start a simulation node when the current binary is 48 // executed as "p2p-node" (rather than whatever the main() function would normally do). 49 reexec.Register("p2p-node", execP2PNode) 50 } 51 52 // ExecAdapter is a NodeAdapter which runs simulation nodes by executing the current binary 53 // as a child process. 54 type ExecAdapter struct { 55 // BaseDir is the directory under which the data directories for each 56 // simulation node are created. 57 BaseDir string 58 59 nodes map[enode.ID]*ExecNode 60 } 61 62 // NewExecAdapter returns an ExecAdapter which stores node data in 63 // subdirectories of the given base directory 64 func NewExecAdapter(baseDir string) *ExecAdapter { 65 return &ExecAdapter{ 66 BaseDir: baseDir, 67 nodes: make(map[enode.ID]*ExecNode), 68 } 69 } 70 71 // Name returns the name of the adapter for logging purposes 72 func (e *ExecAdapter) Name() string { 73 return "exec-adapter" 74 } 75 76 // NewNode returns a new ExecNode using the given config 77 func (e *ExecAdapter) NewNode(config *NodeConfig) (Node, error) { 78 if len(config.Lifecycles) == 0 { 79 return nil, errors.New("node must have at least one service lifecycle") 80 } 81 for _, service := range config.Lifecycles { 82 if _, exists := lifecycleConstructorFuncs[service]; !exists { 83 return nil, fmt.Errorf("unknown node service %q", service) 84 } 85 } 86 87 // create the node directory using the first 12 characters of the ID 88 // as Unix socket paths cannot be longer than 256 characters 89 dir := filepath.Join(e.BaseDir, config.ID.String()[:12]) 90 if err := os.Mkdir(dir, 0755); err != nil { 91 return nil, fmt.Errorf("error creating node directory: %s", err) 92 } 93 94 err := config.initDummyEnode() 95 if err != nil { 96 return nil, err 97 } 98 99 // generate the config 100 conf := &execNodeConfig{ 101 Stack: node.DefaultConfig, 102 Node: config, 103 } 104 if config.DataDir != "" { 105 conf.Stack.DataDir = config.DataDir 106 } else { 107 conf.Stack.DataDir = filepath.Join(dir, "data") 108 } 109 110 // these parameters are crucial for execadapter node to run correctly 111 conf.Stack.WSHost = "127.0.0.1" 112 conf.Stack.WSPort = 0 113 conf.Stack.WSOrigins = []string{"*"} 114 conf.Stack.WSExposeAll = true 115 conf.Stack.P2P.EnableMsgEvents = config.EnableMsgEvents 116 conf.Stack.P2P.NoDiscovery = true 117 conf.Stack.P2P.NAT = nil 118 119 // Listen on a localhost port, which we set when we 120 // initialise NodeConfig (usually a random port) 121 conf.Stack.P2P.ListenAddr = fmt.Sprintf(":%d", config.Port) 122 123 node := &ExecNode{ 124 ID: config.ID, 125 Dir: dir, 126 Config: conf, 127 adapter: e, 128 } 129 node.newCmd = node.execCommand 130 e.nodes[node.ID] = node 131 return node, nil 132 } 133 134 // ExecNode starts a simulation node by exec'ing the current binary and 135 // running the configured services 136 type ExecNode struct { 137 ID enode.ID 138 Dir string 139 Config *execNodeConfig 140 Cmd *exec.Cmd 141 Info *p2p.NodeInfo 142 143 adapter *ExecAdapter 144 client *rpc.Client 145 wsAddr string 146 newCmd func() *exec.Cmd 147 } 148 149 // Addr returns the node's enode URL 150 func (n *ExecNode) Addr() []byte { 151 if n.Info == nil { 152 return nil 153 } 154 return []byte(n.Info.Enode) 155 } 156 157 // Client returns an rpc.Client which can be used to communicate with the 158 // underlying services (it is set once the node has started) 159 func (n *ExecNode) Client() (*rpc.Client, error) { 160 return n.client, nil 161 } 162 163 // Start exec's the node passing the ID and service as command line arguments 164 // and the node config encoded as JSON in an environment variable. 165 func (n *ExecNode) Start(snapshots map[string][]byte) (err error) { 166 if n.Cmd != nil { 167 return errors.New("already started") 168 } 169 defer func() { 170 if err != nil { 171 n.Stop() 172 } 173 }() 174 175 // encode a copy of the config containing the snapshot 176 confCopy := *n.Config 177 confCopy.Snapshots = snapshots 178 confCopy.PeerAddrs = make(map[string]string) 179 for id, node := range n.adapter.nodes { 180 confCopy.PeerAddrs[id.String()] = node.wsAddr 181 } 182 confData, err := json.Marshal(confCopy) 183 if err != nil { 184 return fmt.Errorf("error generating node config: %s", err) 185 } 186 // expose the admin namespace via websocket if it's not enabled 187 exposed := confCopy.Stack.WSExposeAll 188 if !exposed { 189 for _, api := range confCopy.Stack.WSModules { 190 if api == "admin" { 191 exposed = true 192 break 193 } 194 } 195 } 196 if !exposed { 197 confCopy.Stack.WSModules = append(confCopy.Stack.WSModules, "admin") 198 } 199 // start the one-shot server that waits for startup information 200 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 201 defer cancel() 202 statusURL, statusC := n.waitForStartupJSON(ctx) 203 204 // start the node 205 cmd := n.newCmd() 206 cmd.Stdout = os.Stdout 207 cmd.Stderr = os.Stderr 208 cmd.Env = append(os.Environ(), 209 envStatusURL+"="+statusURL, 210 envNodeConfig+"="+string(confData), 211 ) 212 if err := cmd.Start(); err != nil { 213 return fmt.Errorf("error starting node: %s", err) 214 } 215 n.Cmd = cmd 216 217 // Wait for the node to start. 218 status := <-statusC 219 if status.Err != "" { 220 return errors.New(status.Err) 221 } 222 client, err := rpc.DialWebsocket(ctx, status.WSEndpoint, "") 223 if err != nil { 224 return fmt.Errorf("can't connect to RPC server: %v", err) 225 } 226 227 // Node ready :) 228 n.client = client 229 n.wsAddr = status.WSEndpoint 230 n.Info = status.NodeInfo 231 return nil 232 } 233 234 // waitForStartupJSON runs a one-shot HTTP server to receive a startup report. 235 func (n *ExecNode) waitForStartupJSON(ctx context.Context) (string, chan nodeStartupJSON) { 236 var ( 237 ch = make(chan nodeStartupJSON, 1) 238 quitOnce sync.Once 239 srv http.Server 240 ) 241 l, err := net.Listen("tcp", "127.0.0.1:0") 242 if err != nil { 243 ch <- nodeStartupJSON{Err: err.Error()} 244 return "", ch 245 } 246 quit := func(status nodeStartupJSON) { 247 quitOnce.Do(func() { 248 l.Close() 249 ch <- status 250 }) 251 } 252 srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 253 var status nodeStartupJSON 254 if err := json.NewDecoder(r.Body).Decode(&status); err != nil { 255 status.Err = fmt.Sprintf("can't decode startup report: %v", err) 256 } 257 quit(status) 258 }) 259 // Run the HTTP server, but don't wait forever and shut it down 260 // if the context is canceled. 261 go srv.Serve(l) 262 go func() { 263 <-ctx.Done() 264 quit(nodeStartupJSON{Err: "didn't get startup report"}) 265 }() 266 267 url := "http://" + l.Addr().String() 268 return url, ch 269 } 270 271 // execCommand returns a command which runs the node locally by exec'ing 272 // the current binary but setting argv[0] to "p2p-node" so that the child 273 // runs execP2PNode 274 func (n *ExecNode) execCommand() *exec.Cmd { 275 return &exec.Cmd{ 276 Path: reexec.Self(), 277 Args: []string{"p2p-node", strings.Join(n.Config.Node.Lifecycles, ","), n.ID.String()}, 278 } 279 } 280 281 // Stop stops the node by first sending SIGTERM and then SIGKILL if the node 282 // doesn't stop within 5s 283 func (n *ExecNode) Stop() error { 284 if n.Cmd == nil { 285 return nil 286 } 287 defer func() { 288 n.Cmd = nil 289 }() 290 291 if n.client != nil { 292 n.client.Close() 293 n.client = nil 294 n.wsAddr = "" 295 n.Info = nil 296 } 297 298 if err := n.Cmd.Process.Signal(syscall.SIGTERM); err != nil { 299 return n.Cmd.Process.Kill() 300 } 301 waitErr := make(chan error, 1) 302 go func() { 303 waitErr <- n.Cmd.Wait() 304 }() 305 select { 306 case err := <-waitErr: 307 return err 308 case <-time.After(5 * time.Second): 309 return n.Cmd.Process.Kill() 310 } 311 } 312 313 // NodeInfo returns information about the node 314 func (n *ExecNode) NodeInfo() *p2p.NodeInfo { 315 info := &p2p.NodeInfo{ 316 ID: n.ID.String(), 317 } 318 if n.client != nil { 319 n.client.Call(&info, "admin_nodeInfo") 320 } 321 return info 322 } 323 324 // ServeRPC serves RPC requests over the given connection by dialling the 325 // node's WebSocket address and joining the two connections 326 func (n *ExecNode) ServeRPC(clientConn *websocket.Conn) error { 327 conn, _, err := websocket.DefaultDialer.Dial(n.wsAddr, nil) 328 if err != nil { 329 return err 330 } 331 var wg sync.WaitGroup 332 wg.Add(2) 333 go wsCopy(&wg, conn, clientConn) 334 go wsCopy(&wg, clientConn, conn) 335 wg.Wait() 336 conn.Close() 337 return nil 338 } 339 340 func wsCopy(wg *sync.WaitGroup, src, dst *websocket.Conn) { 341 defer wg.Done() 342 for { 343 msgType, r, err := src.NextReader() 344 if err != nil { 345 return 346 } 347 w, err := dst.NextWriter(msgType) 348 if err != nil { 349 return 350 } 351 if _, err = io.Copy(w, r); err != nil { 352 return 353 } 354 } 355 } 356 357 // Snapshots creates snapshots of the services by calling the 358 // simulation_snapshot RPC method 359 func (n *ExecNode) Snapshots() (map[string][]byte, error) { 360 if n.client == nil { 361 return nil, errors.New("RPC not started") 362 } 363 var snapshots map[string][]byte 364 return snapshots, n.client.Call(&snapshots, "simulation_snapshot") 365 } 366 367 // execNodeConfig is used to serialize the node configuration so it can be 368 // passed to the child process as a JSON encoded environment variable 369 type execNodeConfig struct { 370 Stack node.Config `json:"stack"` 371 Node *NodeConfig `json:"node"` 372 Snapshots map[string][]byte `json:"snapshots,omitempty"` 373 PeerAddrs map[string]string `json:"peer_addrs,omitempty"` 374 } 375 376 func initLogging() { 377 // Initialize the logging by default first. 378 glogger := log.NewGlogHandler(log.StreamHandler(os.Stderr, log.LogfmtFormat())) 379 glogger.Verbosity(log.LvlInfo) 380 log.Root().SetHandler(glogger) 381 382 confEnv := os.Getenv(envNodeConfig) 383 if confEnv == "" { 384 return 385 } 386 var conf execNodeConfig 387 if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { 388 return 389 } 390 var writer = os.Stderr 391 if conf.Node.LogFile != "" { 392 logWriter, err := os.Create(conf.Node.LogFile) 393 if err != nil { 394 return 395 } 396 writer = logWriter 397 } 398 var verbosity = log.LvlInfo 399 if conf.Node.LogVerbosity <= log.LvlTrace && conf.Node.LogVerbosity >= log.LvlCrit { 400 verbosity = conf.Node.LogVerbosity 401 } 402 // Reinitialize the logger 403 glogger = log.NewGlogHandler(log.StreamHandler(writer, log.TerminalFormat(true))) 404 glogger.Verbosity(verbosity) 405 log.Root().SetHandler(glogger) 406 } 407 408 // execP2PNode starts a simulation node when the current binary is executed with 409 // argv[0] being "p2p-node", reading the service / ID from argv[1] / argv[2] 410 // and the node config from an environment variable. 411 func execP2PNode() { 412 initLogging() 413 414 statusURL := os.Getenv(envStatusURL) 415 if statusURL == "" { 416 log.Crit("missing " + envStatusURL) 417 } 418 419 // Start the node and gather startup report. 420 var status nodeStartupJSON 421 stack, stackErr := startExecNodeStack() 422 if stackErr != nil { 423 status.Err = stackErr.Error() 424 } else { 425 status.WSEndpoint = stack.WSEndpoint() 426 status.NodeInfo = stack.Server().NodeInfo() 427 } 428 429 // Send status to the host. 430 statusJSON, _ := json.Marshal(status) 431 if _, err := http.Post(statusURL, "application/json", bytes.NewReader(statusJSON)); err != nil { 432 log.Crit("Can't post startup info", "url", statusURL, "err", err) 433 } 434 if stackErr != nil { 435 os.Exit(1) 436 } 437 438 // Stop the stack if we get a SIGTERM signal. 439 go func() { 440 sigc := make(chan os.Signal, 1) 441 signal.Notify(sigc, syscall.SIGTERM) 442 defer signal.Stop(sigc) 443 <-sigc 444 log.Info("Received SIGTERM, shutting down...") 445 stack.Close() 446 }() 447 stack.Wait() // Wait for the stack to exit. 448 } 449 450 func startExecNodeStack() (*node.Node, error) { 451 // read the services from argv 452 serviceNames := strings.Split(os.Args[1], ",") 453 454 // decode the config 455 confEnv := os.Getenv(envNodeConfig) 456 if confEnv == "" { 457 return nil, fmt.Errorf("missing " + envNodeConfig) 458 } 459 var conf execNodeConfig 460 if err := json.Unmarshal([]byte(confEnv), &conf); err != nil { 461 return nil, fmt.Errorf("error decoding %s: %v", envNodeConfig, err) 462 } 463 464 // create enode record 465 nodeTcpConn, _ := net.ResolveTCPAddr("tcp", conf.Stack.P2P.ListenAddr) 466 if nodeTcpConn.IP == nil { 467 nodeTcpConn.IP = net.IPv4(127, 0, 0, 1) 468 } 469 conf.Node.initEnode(nodeTcpConn.IP, nodeTcpConn.Port, nodeTcpConn.Port) 470 conf.Stack.P2P.PrivateKey = conf.Node.PrivateKey 471 conf.Stack.Logger = log.New("node.id", conf.Node.ID.String()) 472 473 // initialize the devp2p stack 474 stack, err := node.New(&conf.Stack) 475 if err != nil { 476 return nil, fmt.Errorf("error creating node stack: %v", err) 477 } 478 479 // Register the services, collecting them into a map so they can 480 // be accessed by the snapshot API. 481 services := make(map[string]node.Lifecycle, len(serviceNames)) 482 for _, name := range serviceNames { 483 lifecycleFunc, exists := lifecycleConstructorFuncs[name] 484 if !exists { 485 return nil, fmt.Errorf("unknown node service %q", err) 486 } 487 ctx := &ServiceContext{ 488 RPCDialer: &wsRPCDialer{addrs: conf.PeerAddrs}, 489 Config: conf.Node, 490 } 491 if conf.Snapshots != nil { 492 ctx.Snapshot = conf.Snapshots[name] 493 } 494 service, err := lifecycleFunc(ctx, stack) 495 if err != nil { 496 return nil, err 497 } 498 services[name] = service 499 } 500 501 // Add the snapshot API. 502 stack.RegisterAPIs([]rpc.API{{ 503 Namespace: "simulation", 504 Version: "1.0", 505 Service: SnapshotAPI{services}, 506 }}) 507 508 if err = stack.Start(); err != nil { 509 err = fmt.Errorf("error starting stack: %v", err) 510 } 511 return stack, err 512 } 513 514 const ( 515 envStatusURL = "_P2P_STATUS_URL" 516 envNodeConfig = "_P2P_NODE_CONFIG" 517 ) 518 519 // nodeStartupJSON is sent to the simulation host after startup. 520 type nodeStartupJSON struct { 521 Err string 522 WSEndpoint string 523 NodeInfo *p2p.NodeInfo 524 } 525 526 // SnapshotAPI provides an RPC method to create snapshots of services 527 type SnapshotAPI struct { 528 services map[string]node.Lifecycle 529 } 530 531 func (api SnapshotAPI) Snapshot() (map[string][]byte, error) { 532 snapshots := make(map[string][]byte) 533 for name, service := range api.services { 534 if s, ok := service.(interface { 535 Snapshot() ([]byte, error) 536 }); ok { 537 snap, err := s.Snapshot() 538 if err != nil { 539 return nil, err 540 } 541 snapshots[name] = snap 542 } 543 } 544 return snapshots, nil 545 } 546 547 type wsRPCDialer struct { 548 addrs map[string]string 549 } 550 551 // DialRPC implements the RPCDialer interface by creating a WebSocket RPC 552 // client of the given node 553 func (w *wsRPCDialer) DialRPC(id enode.ID) (*rpc.Client, error) { 554 addr, ok := w.addrs[id.String()] 555 if !ok { 556 return nil, fmt.Errorf("unknown node: %s", id) 557 } 558 return rpc.DialWebsocket(context.Background(), addr, "http://localhost") 559 }