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