github.com/ava-labs/avalanchego@v1.11.11/vms/rpcchainvm/runtime/subprocess/runtime.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package subprocess 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "net" 11 "os" 12 "os/exec" 13 "strings" 14 "time" 15 16 "go.uber.org/zap" 17 18 "github.com/ava-labs/avalanchego/utils/logging" 19 "github.com/ava-labs/avalanchego/vms/rpcchainvm/grpcutils" 20 "github.com/ava-labs/avalanchego/vms/rpcchainvm/gruntime" 21 "github.com/ava-labs/avalanchego/vms/rpcchainvm/runtime" 22 23 pb "github.com/ava-labs/avalanchego/proto/pb/vm/runtime" 24 ) 25 26 type Config struct { 27 // Stderr of the VM process written to this writer. 28 Stderr io.Writer 29 // Stdout of the VM process written to this writer. 30 Stdout io.Writer 31 // Duration engine server will wait for handshake success. 32 HandshakeTimeout time.Duration 33 Log logging.Logger 34 } 35 36 type Status struct { 37 // Id of the process. 38 Pid int 39 // Address of the VM gRPC service. 40 Addr string 41 } 42 43 // Bootstrap starts a VM as a subprocess after initialization completes and 44 // pipes the IO to the appropriate writers. 45 // 46 // The subprocess is expected to be stopped by the caller if a non-nil error is 47 // returned. If piping the IO fails then the subprocess will be stopped. 48 // 49 // TODO: create the listener inside this method once we refactor the tests 50 func Bootstrap( 51 ctx context.Context, 52 listener net.Listener, 53 cmd *exec.Cmd, 54 config *Config, 55 ) (*Status, runtime.Stopper, error) { 56 defer listener.Close() 57 58 switch { 59 case cmd == nil: 60 return nil, nil, fmt.Errorf("%w: cmd required", runtime.ErrInvalidConfig) 61 case config.Log == nil: 62 return nil, nil, fmt.Errorf("%w: logger required", runtime.ErrInvalidConfig) 63 case config.Stderr == nil, config.Stdout == nil: 64 return nil, nil, fmt.Errorf("%w: stderr and stdout required", runtime.ErrInvalidConfig) 65 } 66 67 intitializer := newInitializer(cmd.Path) 68 69 server := grpcutils.NewServer() 70 defer server.GracefulStop() 71 pb.RegisterRuntimeServer(server, gruntime.NewServer(intitializer)) 72 73 go grpcutils.Serve(listener, server) 74 75 serverAddr := listener.Addr() 76 cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", runtime.EngineAddressKey, serverAddr.String())) 77 // pass golang debug env to subprocess 78 for _, env := range os.Environ() { 79 if strings.HasPrefix(env, "GRPC_") || strings.HasPrefix(env, "GODEBUG") { 80 cmd.Env = append(cmd.Env, env) 81 } 82 } 83 84 stdoutPipe, err := cmd.StdoutPipe() 85 if err != nil { 86 return nil, nil, fmt.Errorf("failed to create stdout pipe: %w", err) 87 } 88 stderrPipe, err := cmd.StderrPipe() 89 if err != nil { 90 return nil, nil, fmt.Errorf("failed to create stderr pipe: %w", err) 91 } 92 93 // start subproccess 94 if err := cmd.Start(); err != nil { 95 return nil, nil, fmt.Errorf("failed to start process: %w", err) 96 } 97 98 log := config.Log 99 stopper := NewStopper(log, cmd) 100 101 // start stdout collector 102 go func() { 103 _, err := io.Copy(config.Stdout, stdoutPipe) 104 if err != nil { 105 log.Error("stdout collector failed", 106 zap.Error(err), 107 ) 108 } 109 stopper.Stop(context.TODO()) 110 111 log.Info("stdout collector shutdown") 112 }() 113 114 // start stderr collector 115 go func() { 116 _, err := io.Copy(config.Stderr, stderrPipe) 117 if err != nil { 118 log.Error("stderr collector failed", 119 zap.Error(err), 120 ) 121 } 122 stopper.Stop(context.TODO()) 123 124 log.Info("stderr collector shutdown") 125 }() 126 127 // wait for handshake success 128 timeout := time.NewTimer(config.HandshakeTimeout) 129 defer timeout.Stop() 130 131 select { 132 case <-intitializer.initialized: 133 case <-timeout.C: 134 stopper.Stop(ctx) 135 return nil, nil, fmt.Errorf("%w: %w", runtime.ErrHandshakeFailed, runtime.ErrProcessNotFound) 136 } 137 138 if intitializer.err != nil { 139 stopper.Stop(ctx) 140 return nil, nil, fmt.Errorf("%w: %w", runtime.ErrHandshakeFailed, intitializer.err) 141 } 142 143 log.Info("plugin handshake succeeded", 144 zap.String("addr", intitializer.vmAddr), 145 ) 146 147 status := &Status{ 148 Pid: cmd.Process.Pid, 149 Addr: intitializer.vmAddr, 150 } 151 return status, stopper, nil 152 }