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 }