dcrd/rpctest/node.go
Dave Collins 2b760c17e3
multi: Address some linter complaints.
This makes the following changes pointed out by linting:

- Uses ++ instead of += 1 for incrementing the expiry in mempool test
  code
- Removes an unused function in the rpc test code
- No need to specify variable type since it is inferred
2021-11-15 16:18:41 -06:00

407 lines
9.8 KiB
Go

// Copyright (c) 2016 The btcsuite developers
// Copyright (c) 2017-2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package rpctest
import (
"bufio"
"crypto/elliptic"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"sync"
"testing"
"time"
"github.com/decred/dcrd/certgen"
rpc "github.com/decred/dcrd/rpcclient/v7"
)
// nodeConfig contains all the args, and data required to launch a dcrd process
// and connect the rpc client to it.
type nodeConfig struct {
rpcUser string
rpcPass string
listen string
rpcListen string
rpcConnect string
dataDir string
logDir string
profile string
debugLevel string
extra []string
prefix string
pathToDCRD string
endpoint string
certFile string
keyFile string
certificates []byte
}
// newConfig returns a newConfig with all default values.
func newConfig(prefix, certFile, keyFile string, extra []string) (*nodeConfig, error) {
a := &nodeConfig{
listen: "127.0.0.1:18555",
rpcListen: "127.0.0.1:18556",
rpcUser: "user",
rpcPass: "pass",
extra: extra,
prefix: prefix,
endpoint: "ws",
certFile: certFile,
keyFile: keyFile,
}
if err := a.setDefaults(); err != nil {
return nil, err
}
return a, nil
}
// setDefaults sets the default values of the config. It also creates the
// temporary data, and log directories which must be cleaned up with a call to
// cleanup().
func (n *nodeConfig) setDefaults() error {
n.dataDir = filepath.Join(n.prefix, "data")
n.logDir = filepath.Join(n.prefix, "logs")
cert, err := os.ReadFile(n.certFile)
if err != nil {
return err
}
n.certificates = cert
return nil
}
// arguments returns an array of arguments that be used to launch the dcrd
// process.
func (n *nodeConfig) arguments() []string {
args := []string{}
if n.rpcUser != "" {
// --rpcuser
args = append(args, fmt.Sprintf("--rpcuser=%s", n.rpcUser))
}
if n.rpcPass != "" {
// --rpcpass
args = append(args, fmt.Sprintf("--rpcpass=%s", n.rpcPass))
}
if n.listen != "" {
// --listen
args = append(args, fmt.Sprintf("--listen=%s", n.listen))
}
if n.rpcListen != "" {
// --rpclisten
args = append(args, fmt.Sprintf("--rpclisten=%s", n.rpcListen))
}
if n.rpcConnect != "" {
// --rpcconnect
args = append(args, fmt.Sprintf("--rpcconnect=%s", n.rpcConnect))
}
// --rpccert
args = append(args, fmt.Sprintf("--rpccert=%s", n.certFile))
// --rpckey
args = append(args, fmt.Sprintf("--rpckey=%s", n.keyFile))
// --txindex
args = append(args, "--txindex")
// --addrindex
args = append(args, "--addrindex")
if n.dataDir != "" {
// --datadir
args = append(args, fmt.Sprintf("--datadir=%s", n.dataDir))
}
if n.logDir != "" {
// --logdir
args = append(args, fmt.Sprintf("--logdir=%s", n.logDir))
}
if n.profile != "" {
// --profile
args = append(args, fmt.Sprintf("--profile=%s", n.profile))
}
if n.debugLevel != "" {
// --debuglevel
args = append(args, fmt.Sprintf("--debuglevel=%s", n.debugLevel))
}
// --allowunsyncedmining
args = append(args, "--allowunsyncedmining")
args = append(args, n.extra...)
return args
}
// command returns the exec.Cmd which will be used to start the dcrd process.
func (n *nodeConfig) command() *exec.Cmd {
return exec.Command(n.pathToDCRD, n.arguments()...)
}
// rpcConnConfig returns the rpc connection config that can be used to connect
// to the dcrd process that is launched via Start().
func (n *nodeConfig) rpcConnConfig() rpc.ConnConfig {
return rpc.ConnConfig{
Host: n.rpcListen,
Endpoint: n.endpoint,
User: n.rpcUser,
Pass: n.rpcPass,
Certificates: n.certificates,
DisableAutoReconnect: true,
}
}
// String returns the string representation of this nodeConfig.
func (n *nodeConfig) String() string {
return n.prefix
}
// node houses the necessary state required to configure, launch, and manage a
// dcrd process.
type node struct {
config *nodeConfig
cmd *exec.Cmd
pidFile string
stderr io.ReadCloser
stdout io.ReadCloser
wg sync.WaitGroup
pid int
dataDir string
t *testing.T
}
// logf is identical to n.t.Logf but it prepends the pid of this node.
func (n *node) logf(format string, args ...interface{}) {
pid := strconv.Itoa(n.pid) + " "
logf(n.t, pid+format, args...)
}
// tracef is identical to debug.go.tracef but it prepends the pid of this
// node.
func (n *node) tracef(format string, args ...interface{}) {
if !trace {
return
}
pid := strconv.Itoa(n.pid) + " "
tracef(n.t, pid+format, args...)
}
// buildNode creates a new temporary directory and node and saves the location
// to a package level variable where it is used for all tests. pathToDCRDMtx
// must be held for writes.
func buildNode(t *testing.T) error {
testNodeDir, err := os.MkdirTemp("", "rpctestdcrdnode")
if err != nil {
return err
}
pathToDCRD = filepath.Join(testNodeDir, "dcrd")
if runtime.GOOS == "windows" {
pathToDCRD += ".exe"
}
debugf(t, "test node located at: %v\n", pathToDCRD)
// Determine import path of this package.
_, rpctestDir, _, ok := runtime.Caller(1)
if !ok {
return fmt.Errorf("cannot get path to dcrd source code")
}
dcrdPkgPath := filepath.Join(rpctestDir, "..", "..")
// Build dcrd and output an executable in a static temp path.
cmd := exec.Command("go", "build", "-o", pathToDCRD, dcrdPkgPath)
err = cmd.Run()
if err != nil {
return fmt.Errorf("failed to build dcrd: %v", err)
}
return nil
}
// newNode creates a new node instance according to the passed config. dataDir
// will be used to hold a file recording the pid of the launched process, and
// as the base for the log and data directories for dcrd. If pathToDCRD has a
// non-zero value, the executable located there is used.
func newNode(t *testing.T, config *nodeConfig, dataDir string) (*node, error) {
// Create the dcrd node used for tests if not created yet.
pathToDCRDMtx.Lock()
if pathToDCRD == "" {
if err := buildNode(t); err != nil {
pathToDCRDMtx.Unlock()
return nil, err
}
}
config.pathToDCRD = pathToDCRD
pathToDCRDMtx.Unlock()
return &node{
config: config,
dataDir: dataDir,
cmd: config.command(),
t: t,
}, nil
}
// start creates a new dcrd process, and writes its pid in a file reserved for
// recording the pid of the launched process. 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 panic, it is important that the process be stopped via stop(),
// otherwise, it will persist unless explicitly killed.
func (n *node) start() error {
var err error
var pid sync.WaitGroup
pid.Add(1)
// Redirect stderr.
n.stderr, err = n.cmd.StderrPipe()
if err != nil {
return err
}
n.wg.Add(1)
go func() {
defer n.wg.Done()
pid.Wait() // Block until pid is available
r := bufio.NewReader(n.stderr)
for {
line, err := r.ReadBytes('\n')
if errors.Is(err, io.EOF) {
n.tracef("stderr: EOF")
return
}
n.logf("stderr: %s", line)
}
}()
// Redirect stdout.
n.stdout, err = n.cmd.StdoutPipe()
if err != nil {
return err
}
n.wg.Add(1)
go func() {
defer n.wg.Done()
pid.Wait() // Block until pid is available
r := bufio.NewReader(n.stdout)
for {
line, err := r.ReadBytes('\n')
if errors.Is(err, io.EOF) {
n.tracef("stdout: EOF")
return
}
n.tracef("stdout: %s", line)
}
}()
// Launch command and store pid.
if err := n.cmd.Start(); err != nil {
return err
}
n.pid = n.cmd.Process.Pid
// Unblock pipes now pid is available
pid.Done()
f, err := os.Create(filepath.Join(n.config.String(), "dcrd.pid"))
if err != nil {
return err
}
n.pidFile = f.Name()
if _, err = fmt.Fprintf(f, "%d\n", n.cmd.Process.Pid); err != nil {
return err
}
return f.Close()
}
// stop interrupts the running dcrd process, and waits until it exits
// properly. On windows, interrupt is not supported, so a kill signal is used
// instead
func (n *node) stop() error {
n.tracef("stop %p %p", n.cmd, n.cmd.Process)
defer n.tracef("stop done")
if n.cmd == nil || n.cmd.Process == nil {
// return if not properly initialized
// or error starting the process
return nil
}
// Send kill command
n.tracef("stop send kill")
var err error
if runtime.GOOS == "windows" {
err = n.cmd.Process.Signal(os.Kill)
} else {
err = n.cmd.Process.Signal(os.Interrupt)
}
if err != nil {
n.t.Logf("stop Signal error: %v", err)
}
// Wait for pipes.
n.tracef("stop wg")
n.wg.Wait()
// Wait for command to exit.
n.tracef("stop cmd.Wait")
err = n.cmd.Wait()
if err != nil {
n.t.Logf("stop cmd.Wait error: %v", err)
}
return nil
}
// cleanup cleanups process and args files. The file housing the pid of the
// created process will be deleted, as well as any directories created by the
// process.
func (n *node) cleanup() error {
n.tracef("cleanup")
defer n.tracef("cleanup done")
if n.pidFile != "" {
if err := os.Remove(n.pidFile); err != nil {
n.t.Logf("unable to remove file %s: %v", n.pidFile,
err)
return err
}
}
return nil
}
// shutdown terminates the running dcrd process, and cleans up all
// file/directories created by node.
func (n *node) shutdown() error {
n.tracef("shutdown")
defer n.tracef("shutdown done")
if err := n.stop(); err != nil {
n.t.Logf("shutdown stop error: %v", err)
return err
}
return n.cleanup()
}
// genCertPair generates a key/cert pair to the paths provided.
func genCertPair(certFile, keyFile string) error {
org := "rpctest autogenerated cert"
validUntil := time.Now().Add(10 * 365 * 24 * time.Hour)
cert, key, err := certgen.NewTLSCertPair(elliptic.P521(), org,
validUntil, nil)
if err != nil {
return err
}
// Write cert and key files.
if err = os.WriteFile(certFile, cert, 0644); err != nil {
return err
}
if err = os.WriteFile(keyFile, key, 0600); err != nil {
os.Remove(certFile)
return err
}
return nil
}