github.com/MetalBlockchain/metalgo@v1.11.9/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/MetalBlockchain/metalgo/utils/logging"
    19  	"github.com/MetalBlockchain/metalgo/vms/rpcchainvm/grpcutils"
    20  	"github.com/MetalBlockchain/metalgo/vms/rpcchainvm/gruntime"
    21  	"github.com/MetalBlockchain/metalgo/vms/rpcchainvm/runtime"
    22  
    23  	pb "github.com/MetalBlockchain/metalgo/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()
    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  }