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