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