go.etcd.io/etcd@v3.3.27+incompatible/pkg/expect/expect.go (about)

     1  // Copyright 2016 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package expect implements a small expect-style interface
    16  package expect
    17  
    18  import (
    19  	"bufio"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"strings"
    25  	"sync"
    26  	"syscall"
    27  
    28  	"github.com/kr/pty"
    29  )
    30  
    31  type ExpectProcess struct {
    32  	cmd  *exec.Cmd
    33  	fpty *os.File
    34  	wg   sync.WaitGroup
    35  
    36  	cond  *sync.Cond // for broadcasting updates are available
    37  	mu    sync.Mutex // protects lines and err
    38  	lines []string
    39  	count int // increment whenever new line gets added
    40  	err   error
    41  
    42  	// StopSignal is the signal Stop sends to the process; defaults to SIGKILL.
    43  	StopSignal os.Signal
    44  }
    45  
    46  // NewExpect creates a new process for expect testing.
    47  func NewExpect(name string, arg ...string) (ep *ExpectProcess, err error) {
    48  	// if env[] is nil, use current system env
    49  	return NewExpectWithEnv(name, arg, nil)
    50  }
    51  
    52  // NewExpectWithEnv creates a new process with user defined env variables for expect testing.
    53  func NewExpectWithEnv(name string, args []string, env []string) (ep *ExpectProcess, err error) {
    54  	cmd := exec.Command(name, args...)
    55  	cmd.Env = env
    56  	ep = &ExpectProcess{
    57  		cmd:        cmd,
    58  		StopSignal: syscall.SIGKILL,
    59  	}
    60  	ep.cond = sync.NewCond(&ep.mu)
    61  	ep.cmd.Stderr = ep.cmd.Stdout
    62  	ep.cmd.Stdin = nil
    63  
    64  	if ep.fpty, err = pty.Start(ep.cmd); err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	ep.wg.Add(1)
    69  	go ep.read()
    70  	return ep, nil
    71  }
    72  
    73  func (ep *ExpectProcess) read() {
    74  	defer ep.wg.Done()
    75  	printDebugLines := os.Getenv("EXPECT_DEBUG") != ""
    76  	r := bufio.NewReader(ep.fpty)
    77  	for ep.err == nil {
    78  		l, rerr := r.ReadString('\n')
    79  		ep.mu.Lock()
    80  		ep.err = rerr
    81  		if l != "" {
    82  			if printDebugLines {
    83  				fmt.Printf("%s-%d: %s", ep.cmd.Path, ep.cmd.Process.Pid, l)
    84  			}
    85  			ep.lines = append(ep.lines, l)
    86  			ep.count++
    87  			if len(ep.lines) == 1 {
    88  				ep.cond.Signal()
    89  			}
    90  		}
    91  		ep.mu.Unlock()
    92  	}
    93  	ep.cond.Signal()
    94  }
    95  
    96  // ExpectFunc returns the first line satisfying the function f.
    97  func (ep *ExpectProcess) ExpectFunc(f func(string) bool) (string, error) {
    98  	ep.mu.Lock()
    99  	for {
   100  		for len(ep.lines) == 0 && ep.err == nil {
   101  			ep.cond.Wait()
   102  		}
   103  		if len(ep.lines) == 0 {
   104  			break
   105  		}
   106  		l := ep.lines[0]
   107  		ep.lines = ep.lines[1:]
   108  		if f(l) {
   109  			ep.mu.Unlock()
   110  			return l, nil
   111  		}
   112  	}
   113  	ep.mu.Unlock()
   114  	return "", ep.err
   115  }
   116  
   117  // Expect returns the first line containing the given string.
   118  func (ep *ExpectProcess) Expect(s string) (string, error) {
   119  	return ep.ExpectFunc(func(txt string) bool { return strings.Contains(txt, s) })
   120  }
   121  
   122  // LineCount returns the number of recorded lines since
   123  // the beginning of the process.
   124  func (ep *ExpectProcess) LineCount() int {
   125  	ep.mu.Lock()
   126  	defer ep.mu.Unlock()
   127  	return ep.count
   128  }
   129  
   130  // Stop kills the expect process and waits for it to exit.
   131  func (ep *ExpectProcess) Stop() error { return ep.close(true) }
   132  
   133  // Signal sends a signal to the expect process
   134  func (ep *ExpectProcess) Signal(sig os.Signal) error {
   135  	return ep.cmd.Process.Signal(sig)
   136  }
   137  
   138  // Close waits for the expect process to exit.
   139  func (ep *ExpectProcess) Close() error { return ep.close(false) }
   140  
   141  func (ep *ExpectProcess) close(kill bool) error {
   142  	if ep.cmd == nil {
   143  		return ep.err
   144  	}
   145  	if kill {
   146  		ep.Signal(ep.StopSignal)
   147  	}
   148  
   149  	err := ep.cmd.Wait()
   150  	ep.fpty.Close()
   151  	ep.wg.Wait()
   152  
   153  	if err != nil {
   154  		ep.err = err
   155  		if !kill && strings.Contains(err.Error(), "exit status") {
   156  			// non-zero exit code
   157  			err = nil
   158  		} else if kill && strings.Contains(err.Error(), "signal:") {
   159  			err = nil
   160  		}
   161  	}
   162  	ep.cmd = nil
   163  	return err
   164  }
   165  
   166  func (ep *ExpectProcess) Send(command string) error {
   167  	_, err := io.WriteString(ep.fpty, command)
   168  	return err
   169  }