node.go raw

   1  package rpctest
   2  
   3  import (
   4  	"fmt"
   5  	"io/ioutil"
   6  	"os"
   7  	"os/exec"
   8  	"path/filepath"
   9  	"runtime"
  10  	"time"
  11  	
  12  	rpc "github.com/p9c/p9/pkg/rpcclient"
  13  	"github.com/p9c/p9/pkg/util"
  14  )
  15  
  16  // nodeConfig contains all the args and data required to launch a pod process and connect the rpc client to it.
  17  type nodeConfig struct {
  18  	rpcUser      string
  19  	rpcPass      string
  20  	listen       string
  21  	rpcListen    string
  22  	rpcConnect   string
  23  	dataDir      string
  24  	logDir       string
  25  	profile      string
  26  	debugLevel   string
  27  	extra        []string
  28  	prefix       string
  29  	exe          string
  30  	endpoint     string
  31  	certFile     string
  32  	keyFile      string
  33  	certificates []byte
  34  }
  35  
  36  // newConfig returns a newConfig with all default values.
  37  func newConfig(prefix, certFile, keyFile string, extra []string) (*nodeConfig, error) {
  38  	podPath, e := podExecutablePath()
  39  	if e != nil {
  40  		podPath = "pod"
  41  	}
  42  	a := &nodeConfig{
  43  		listen:    "127.0.0.1:41047",
  44  		rpcListen: "127.0.0.1:41048",
  45  		rpcUser:   "user",
  46  		rpcPass:   "pass",
  47  		extra:     extra,
  48  		prefix:    prefix,
  49  		exe:       podPath,
  50  		endpoint:  "ws",
  51  		certFile:  certFile,
  52  		keyFile:   keyFile,
  53  	}
  54  	if e := a.setDefaults(); E.Chk(e) {
  55  		return nil, e
  56  	}
  57  	return a, nil
  58  }
  59  
  60  // setDefaults sets the default values of the config. It also creates the temporary data, and log directories which must
  61  // be cleaned up with a call to cleanup().
  62  func (n *nodeConfig) setDefaults() (e error) {
  63  	datadir, e := ioutil.TempDir("", n.prefix+"-data")
  64  	if e != nil {
  65  		return e
  66  	}
  67  	n.dataDir = datadir
  68  	logdir, e := ioutil.TempDir("", n.prefix+"-logs")
  69  	if e != nil {
  70  		return e
  71  	}
  72  	n.logDir = logdir
  73  	cert, e := ioutil.ReadFile(n.certFile)
  74  	if e != nil {
  75  		return e
  76  	}
  77  	n.certificates = cert
  78  	return nil
  79  }
  80  
  81  // arguments returns an array of arguments that be used to launch the pod process.
  82  func (n *nodeConfig) arguments() []string {
  83  	args := []string{}
  84  	if n.rpcUser != "" {
  85  		// --rpcuser
  86  		args = append(args, fmt.Sprintf("--rpcuser=%s", n.rpcUser))
  87  	}
  88  	if n.rpcPass != "" {
  89  		// --rpcpass
  90  		args = append(args, fmt.Sprintf("--rpcpass=%s", n.rpcPass))
  91  	}
  92  	if n.listen != "" {
  93  		// --listen
  94  		args = append(args, fmt.Sprintf("--listen=%s", n.listen))
  95  	}
  96  	if n.rpcListen != "" {
  97  		// --rpclisten
  98  		args = append(args, fmt.Sprintf("--rpclisten=%s", n.rpcListen))
  99  	}
 100  	if n.rpcConnect != "" {
 101  		// --rpcconnect
 102  		args = append(args, fmt.Sprintf("--rpcconnect=%s", n.rpcConnect))
 103  	}
 104  	// --rpccert
 105  	args = append(args, fmt.Sprintf("--rpccert=%s", n.certFile))
 106  	// --rpckey
 107  	args = append(args, fmt.Sprintf("--rpckey=%s", n.keyFile))
 108  	if n.dataDir != "" {
 109  		// --datadir
 110  		args = append(args, fmt.Sprintf("--datadir=%s", n.dataDir))
 111  	}
 112  	if n.logDir != "" {
 113  		// --logdir
 114  		args = append(args, fmt.Sprintf("--logdir=%s", n.logDir))
 115  	}
 116  	if n.profile != "" {
 117  		// --profile
 118  		args = append(args, fmt.Sprintf("--profile=%s", n.profile))
 119  	}
 120  	if n.debugLevel != "" {
 121  		// --debuglevel
 122  		args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel))
 123  	}
 124  	args = append(args, n.extra...)
 125  	return args
 126  }
 127  
 128  // command returns the exec.Cmd which will be used to start the pod process.
 129  func (n *nodeConfig) command() *exec.Cmd {
 130  	return exec.Command(n.exe, n.arguments()...)
 131  }
 132  
 133  // rpcConnConfig returns the rpc connection config that can be used to connect to the pod process that is launched via
 134  // Start().
 135  func (n *nodeConfig) rpcConnConfig() rpc.ConnConfig {
 136  	return rpc.ConnConfig{
 137  		Host:                 n.rpcListen,
 138  		Endpoint:             n.endpoint,
 139  		User:                 n.rpcUser,
 140  		Pass:                 n.rpcPass,
 141  		Certificates:         n.certificates,
 142  		DisableAutoReconnect: true,
 143  	}
 144  }
 145  
 146  // String returns the string representation of this nodeConfig.
 147  func (n *nodeConfig) String() string {
 148  	return n.prefix
 149  }
 150  
 151  // cleanup removes the tmp data and log directories.
 152  func (n *nodeConfig) cleanup() (e error) {
 153  	dirs := []string{
 154  		n.logDir,
 155  		n.dataDir,
 156  	}
 157  	for _, dir := range dirs {
 158  		if e = os.RemoveAll(dir); E.Chk(e) {
 159  			E.F("Cannot remove dir %s: %v", dir, e)
 160  		}
 161  	}
 162  	return e
 163  }
 164  
 165  // node houses the necessary state required to configure, launch, and manage a pod process.
 166  type node struct {
 167  	config  *nodeConfig
 168  	cmd     *exec.Cmd
 169  	pidFile string
 170  	dataDir string
 171  }
 172  
 173  // newNode creates a new node instance according to the passed config. dataDir will be used to hold a file recording the
 174  // pid of the launched process, and as the base for the log and data directories for pod.
 175  func newNode(config *nodeConfig, dataDir string) (*node, error) {
 176  	return &node{
 177  		config:  config,
 178  		dataDir: dataDir,
 179  		cmd:     config.command(),
 180  	}, nil
 181  }
 182  
 183  // start creates a new pod process and writes its pid in a file reserved for recording the pid of the launched process.
 184  // This file can be used to terminate the process in case of a hang or panic. In the case of a failing test case, or
 185  // panic, it is important that the process be stopped via stop( ) otherwise it will persist unless explicitly killed.
 186  func (n *node) start() (e error) {
 187  	if e = n.cmd.Start(); E.Chk(e) {
 188  		return e
 189  	}
 190  	pid, e := os.Create(
 191  		filepath.Join(
 192  			n.dataDir,
 193  			fmt.Sprintf("%s.pid", n.config),
 194  		),
 195  	)
 196  	if e != nil {
 197  		return e
 198  	}
 199  	n.pidFile = pid.Name()
 200  	if _, e = fmt.Fprintf(pid, "%d\n", n.cmd.Process.Pid); E.Chk(e) {
 201  		return e
 202  	}
 203  	if e = pid.Close(); E.Chk(e) {
 204  		return e
 205  	}
 206  	return nil
 207  }
 208  
 209  // stop interrupts the running pod process process, and waits until it exits properly. On windows, interrupt is not
 210  // supported so a kill signal is used instead
 211  func (n *node) stop() (e error) {
 212  	if n.cmd == nil || n.cmd.Process == nil {
 213  		// return if not properly initialized or error starting the process
 214  		return nil
 215  	}
 216  	defer func() {
 217  		e := n.cmd.Wait()
 218  		if e != nil {
 219  		}
 220  	}()
 221  	if runtime.GOOS == "windows" {
 222  		return n.cmd.Process.Signal(os.Kill)
 223  	}
 224  	return n.cmd.Process.Signal(os.Interrupt)
 225  }
 226  
 227  // cleanup cleanups process and args files. The file housing the pid of the created process will be deleted as well as
 228  // any directories created by the process.
 229  func (n *node) cleanup() (e error) {
 230  	if n.pidFile != "" {
 231  		if e := os.Remove(n.pidFile); E.Chk(e) {
 232  			E.F("unable to remove file %s: %v", n.pidFile, e)
 233  		}
 234  	}
 235  	return n.config.cleanup()
 236  }
 237  
 238  // shutdown terminates the running pod process and cleans up all file/directories created by node.
 239  func (n *node) shutdown() (e error) {
 240  	if e := n.stop(); E.Chk(e) {
 241  		return e
 242  	}
 243  	if e := n.cleanup(); E.Chk(e) {
 244  		return e
 245  	}
 246  	return nil
 247  }
 248  
 249  // genCertPair generates a key/cert pair to the paths provided.
 250  func genCertPair(certFile, keyFile string) (e error) {
 251  	org := "rpctest autogenerated cert"
 252  	validUntil := time.Now().Add(10 * 365 * 24 * time.Hour)
 253  	var key []byte
 254  	var cert []byte
 255  	cert, key, e = util.NewTLSCertPair(org, validUntil, nil)
 256  	if e != nil {
 257  		return e
 258  	}
 259  	// Write cert and key files.
 260  	if e = ioutil.WriteFile(certFile, cert, 0666); E.Chk(e) {
 261  		return e
 262  	}
 263  	if e = ioutil.WriteFile(keyFile, key, 0600); E.Chk(e) {
 264  		defer func() {
 265  			if e = os.Remove(certFile); E.Chk(e) {
 266  			}
 267  		}()
 268  		return e
 269  	}
 270  	return nil
 271  }
 272