├── .circleci └── config.yml ├── AUTHORS ├── CONTRIBUTING ├── LICENSE ├── README.md ├── examples ├── newspawner │ └── telnet.go └── process │ └── process.go ├── expect.go ├── expect_test.go ├── go.mod ├── go.sum └── testdata ├── menuloop.sh └── traptest.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # 2 | # YAML anchor to keep configuration DRY. 3 | # 4 | common: &common 5 | # This conforms to Go workspace requirements (we are pre-modules). 6 | working_directory: /go/src/github.com/google/goexpect 7 | steps: 8 | - checkout 9 | - run: 10 | name: Go version 11 | command: go version 12 | - run: 13 | name: Get dependencies 14 | command: go get -v ./... 15 | - run: 16 | name: Run unit tests 17 | command: go test -v ./... 18 | 19 | common_modules: &common_modules 20 | steps: 21 | - checkout 22 | - run: 23 | name: Go version 24 | command: go version 25 | - run: 26 | name: Run unit tests 27 | command: go test -v ./... 28 | 29 | 30 | # 31 | # CircleCI. 32 | # 33 | version: 2 34 | 35 | jobs: 36 | go-1.11: 37 | docker: 38 | - image: circleci/golang:1.11 39 | <<: *common 40 | go-1.12: 41 | docker: 42 | - image: circleci/golang:1.12 43 | <<: *common 44 | go-1.14: 45 | docker: 46 | - image: circleci/golang:1.14 47 | <<: *common 48 | go-1.11_modules: 49 | docker: 50 | - image: circleci/golang:1.11 51 | <<: *common_modules 52 | go-1.12_modules: 53 | docker: 54 | - image: circleci/golang:1.12 55 | <<: *common_modules 56 | go-1.14_modules: 57 | docker: 58 | - image: circleci/golang:1.14 59 | <<: *common_modules 60 | go-latest_modules: 61 | docker: 62 | - image: circleci/golang:latest 63 | <<: *common_modules 64 | 65 | workflows: 66 | version: 2 67 | test: 68 | # These will run in parallel. 69 | jobs: 70 | - go-1.11 71 | - go-1.12 72 | - go-1.14 73 | - go-1.11_modules 74 | - go-1.12_modules 75 | - go-1.14_modules 76 | - go-latest_modules 77 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at the end). 2 | 3 | ### Before you contribute 4 | Before we can use your code, you must sign the 5 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 6 | (CLA), which you can do online. The CLA is necessary mainly because you own the 7 | copyright to your changes, even after your contribution becomes part of our 8 | codebase, so we need your permission to use and distribute your code. We also 9 | need to be sure of various other things—for instance that you'll tell us if you 10 | know that your code infringes on other people's patents. You don't have to sign 11 | the CLA until after you've submitted your code for review and a member has 12 | approved it, but you must do it before we can put your code into our codebase. 13 | Before you start working on a larger contribution, you should get in touch with 14 | us first through the issue tracker with your idea so that we can help out and 15 | possibly guide you. Coordinating up front makes it much easier to avoid 16 | frustration later on. 17 | 18 | ### Code reviews 19 | All submissions, including submissions by project members, require review. We 20 | use Github pull requests for this purpose. 21 | 22 | ### The small print 23 | Contributions made by corporations are covered by a different agreement than 24 | the one above, the 25 | [Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate). 26 | Contact GitHub API Training Shop Blog About 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/google/goexpect.svg?style=svg)](https://circleci.com/gh/google/goexpect) 2 | 3 | This package is an implementation of [Expect](https://en.wikipedia.org/wiki/Expect) in [Go](https://golang.org). 4 | 5 | 6 | ## Features: 7 | - Spawning and controlling local processes with real PTYs. 8 | - Native SSH spawner. 9 | - Expect backed spawner for testing. 10 | - Generic spawner to make implementing additional Spawners simple. 11 | - Has a batcher for implementing workflows without having to write extra logic 12 | and code. 13 | 14 | ## Options 15 | 16 | All Spawn functions accept a variadic of type expect.Option , these are used for changing 17 | options of the Expecter. 18 | 19 | ### CheckDuration 20 | 21 | The Go Expecter checks for new data every two seconds as default. This can be changed by using 22 | the CheckDuration `func CheckDuration(d time.Duration) Option`. 23 | 24 | ### Verbose 25 | 26 | The Verbose option is used to turn on/off verbose logging for Expect/Send statements. 27 | This option can be very useful when troubleshooting workflows since it will log every interaction 28 | with the device. 29 | 30 | ### VerboseWriter 31 | 32 | The VerboseWriter option can be used to change where the verbose session logs are written. 33 | Using this option will start writing verbose output to the provided io.Writer instead of the log default. 34 | 35 | See the [ExampleVerbose](https://github.com/google/goexpect/blob/5c8d637b0287a2ae7bb805554056728c453871e4/expect_test.go#L585) code for an example of how to use this. 36 | 37 | ### NoCheck 38 | 39 | The Go Expecter periodically checks that the spawned process/ssh/session/telnet etc. session is alive. 40 | This option turns that check off. 41 | 42 | ### DebugCheck 43 | 44 | The DebugCheck option adds debugging to the alive Check done by the Expecter, this will start logging information 45 | every time the check is run. Can be used for troubleshooting and debugging of Spawners. 46 | 47 | ### ChangeCheck 48 | 49 | The ChangeCheck option makes it possible to replace the Spawner Check function with a brand new one. 50 | 51 | ### SendTimeout 52 | 53 | The SendTimeout set timeout on the `Send` command, without timeout the `Send` command will wait forewer for the expecter process. 54 | 55 | ### BufferSize 56 | 57 | The BufferSize option provides a mechanism to configure the client io buffer size in bytes. 58 | 59 | ## Basic Examples 60 | 61 | ### networkbit.ch 62 | 63 | An [article](http://networkbit.ch/golang-regular-expression/) with some examples was written about goexpect on [networkbit.ch](http://networkbit.ch). 64 | 65 | ### The [Wikipedia Expect](https://en.wikipedia.org/wiki/Expect) examples. 66 | 67 | #### Telnet 68 | 69 | First we try to replicate the Telnet example from wikipedia as close as possible. 70 | 71 | Interaction: 72 | 73 | ```diff 74 | + username: 75 | - user\n 76 | + password: 77 | - pass\n 78 | + % 79 | - cmd\n 80 | + % 81 | - exit\n 82 | ``` 83 | 84 | *Error checking was omitted to keep the example short* 85 | 86 | ``` 87 | package main 88 | 89 | import ( 90 | "flag" 91 | "fmt" 92 | "log" 93 | "regexp" 94 | "time" 95 | 96 | "github.com/google/goexpect" 97 | "github.com/google/goterm/term" 98 | ) 99 | 100 | const ( 101 | timeout = 10 * time.Minute 102 | ) 103 | 104 | var ( 105 | addr = flag.String("address", "", "address of telnet server") 106 | user = flag.String("user", "", "username to use") 107 | pass = flag.String("pass", "", "password to use") 108 | cmd = flag.String("cmd", "", "command to run") 109 | 110 | userRE = regexp.MustCompile("username:") 111 | passRE = regexp.MustCompile("password:") 112 | promptRE = regexp.MustCompile("%") 113 | ) 114 | 115 | func main() { 116 | flag.Parse() 117 | fmt.Println(term.Bluef("Telnet 1 example")) 118 | 119 | e, _, err := expect.Spawn(fmt.Sprintf("telnet %s", *addr), -1) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | defer e.Close() 124 | 125 | e.Expect(userRE, timeout) 126 | e.Send(*user + "\n") 127 | e.Expect(passRE, timeout) 128 | e.Send(*pass + "\n") 129 | e.Expect(promptRE, timeout) 130 | e.Send(*cmd + "\n") 131 | result, _, _ := e.Expect(promptRE, timeout) 132 | e.Send("exit\n") 133 | 134 | fmt.Println(term.Greenf("%s: result: %s\n", *cmd, result)) 135 | } 136 | 137 | ``` 138 | 139 | In essence to run and attach to a process the `expect.Spawn(,)` is used. 140 | The spawn returns an Expecter `e` that can run `e.Expect` and `e.Send` commands to match information 141 | in the output and Send information in. 142 | 143 | *See the https://github.com/google/goexpect/blob/master/examples/newspawner/telnet.go example for a slightly more fleshed out version* 144 | 145 | #### FTP 146 | 147 | For the FTP example we use the expect.Batch for the following interaction. 148 | 149 | ```diff 150 | + username: 151 | - user\n 152 | + password: 153 | - pass\n 154 | + ftp> 155 | - prompt\n 156 | + ftp> 157 | - mget *\n 158 | + ftp>' 159 | - bye\n 160 | ``` 161 | 162 | *ftp_example.go* 163 | 164 | ``` 165 | package main 166 | 167 | import ( 168 | "flag" 169 | "fmt" 170 | "log" 171 | "time" 172 | 173 | "github.com/google/goexpect" 174 | "github.com/google/goterm/term" 175 | ) 176 | 177 | const ( 178 | timeout = 10 * time.Minute 179 | ) 180 | 181 | var ( 182 | addr = flag.String("address", "", "address of telnet server") 183 | user = flag.String("user", "", "username to use") 184 | pass = flag.String("pass", "", "password to use") 185 | ) 186 | 187 | func main() { 188 | flag.Parse() 189 | fmt.Println(term.Bluef("Ftp 1 example")) 190 | 191 | e, _, err := expect.Spawn(fmt.Sprintf("ftp %s", *addr), -1) 192 | if err != nil { 193 | log.Fatal(err) 194 | } 195 | defer e.Close() 196 | 197 | e.ExpectBatch([]expect.Batcher{ 198 | &expect.BExp{R: "username:"}, 199 | &expect.BSnd{S: *user + "\n"}, 200 | &expect.BExp{R: "password:"}, 201 | &expect.BSnd{S: *pass + "\n"}, 202 | &expect.BExp{R: "ftp>"}, 203 | &expect.BSnd{S: "bin\n"}, 204 | &expect.BExp{R: "ftp>"}, 205 | &expect.BSnd{S: "prompt\n"}, 206 | &expect.BExp{R: "ftp>"}, 207 | &expect.BSnd{S: "mget *\n"}, 208 | &expect.BExp{R: "ftp>"}, 209 | &expect.BSnd{S: "bye\n"}, 210 | }, timeout) 211 | 212 | fmt.Println(term.Greenf("All done")) 213 | } 214 | 215 | ``` 216 | 217 | Using the expect.Batcher makes the standard Send/Expect interactions more compact and simpler to write. 218 | 219 | #### SSH 220 | 221 | With the SSH login example we test out the [expect.Caser](https://github.com/google/goexpect/blob/7f68e6ee0bc89860ff53a5c0d50bcfae61853506/expect.go#L388-L397) 222 | and the [Case Tags](https://github.com/google/goexpect/blob/7f68e6ee0bc89860ff53a5c0d50bcfae61853506/expect.go#L324-L335). 223 | 224 | Also for this we'll use the Go Expect native [SSH Spawner](https://github.com/google/goexpect/blob/7f68e6ee0bc89860ff53a5c0d50bcfae61853506/expect.go#L872-L879) 225 | instead of spawning a process. 226 | 227 | 228 | Interaction: 229 | 230 | ```diff 231 | + "Login: " 232 | - user 233 | + "Password: " 234 | - pass1 235 | + "Wrong password" 236 | + "Login" 237 | - user 238 | + "Password: " 239 | - pass2 240 | + router# 241 | ``` 242 | 243 | *ssh_example.go* 244 | 245 | ``` 246 | package main 247 | 248 | import ( 249 | "flag" 250 | "fmt" 251 | "log" 252 | "regexp" 253 | "time" 254 | 255 | "golang.org/x/crypto/ssh" 256 | 257 | "google.golang.org/grpc/codes" 258 | 259 | "github.com/google/goexpect" 260 | "github.com/google/goterm/term" 261 | ) 262 | 263 | const ( 264 | timeout = 10 * time.Minute 265 | ) 266 | 267 | var ( 268 | addr = flag.String("address", "", "address of telnet server") 269 | user = flag.String("user", "user", "username to use") 270 | pass1 = flag.String("pass1", "pass1", "password to use") 271 | pass2 = flag.String("pass2", "pass2", "alternate password to use") 272 | ) 273 | 274 | func main() { 275 | flag.Parse() 276 | fmt.Println(term.Bluef("SSH Example")) 277 | 278 | sshClt, err := ssh.Dial("tcp", *addr, &ssh.ClientConfig{ 279 | User: *user, 280 | Auth: []ssh.AuthMethod{ssh.Password(*pass1)}, 281 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 282 | }) 283 | if err != nil { 284 | log.Fatalf("ssh.Dial(%q) failed: %v", *addr, err) 285 | } 286 | defer sshClt.Close() 287 | 288 | e, _, err := expect.SpawnSSH(sshClt, timeout) 289 | if err != nil { 290 | log.Fatal(err) 291 | } 292 | defer e.Close() 293 | 294 | e.ExpectBatch([]expect.Batcher{ 295 | &expect.BCas{[]expect.Caser{ 296 | &expect.Case{R: regexp.MustCompile(`router#`), T: expect.OK()}, 297 | &expect.Case{R: regexp.MustCompile(`Login: `), S: *user, 298 | T: expect.Continue(expect.NewStatus(codes.PermissionDenied, "wrong username")), Rt: 3}, 299 | &expect.Case{R: regexp.MustCompile(`Password: `), S: *pass1, T: expect.Next(), Rt: 1}, 300 | &expect.Case{R: regexp.MustCompile(`Password: `), S: *pass2, 301 | T: expect.Continue(expect.NewStatus(codes.PermissionDenied, "wrong password")), Rt: 1}, 302 | }}, 303 | }, timeout) 304 | 305 | fmt.Println(term.Greenf("All done")) 306 | } 307 | ``` 308 | 309 | ### Generic Spawner 310 | 311 | The Go Expect package supports adding new Spawners with the `func SpawnGeneric(opt *GenOptions, timeout time.Duration, opts ...Option) (*GExpect, <-chan error, error)` 312 | function. 313 | 314 | *telnet spawner* 315 | 316 | From the [newspawner](https://github.com/google/goexpect/blob/master/examples/newspawner/telnet.go) example. 317 | 318 | ``` 319 | func telnetSpawn(addr string, timeout time.Duration, opts ...expect.Option) (expect.Expecter, <-chan error, error) { 320 | conn, err := telnet.Dial(network, addr) 321 | if err != nil { 322 | return nil, nil, err 323 | } 324 | 325 | resCh := make(chan error) 326 | 327 | return expect.SpawnGeneric(&expect.GenOptions{ 328 | In: conn, 329 | Out: conn, 330 | Wait: func() error { 331 | return <-resCh 332 | }, 333 | Close: func() error { 334 | close(resCh) 335 | return conn.Close() 336 | }, 337 | Check: func() bool { return true }, 338 | }, timeout, opts...) 339 | } 340 | ``` 341 | 342 | ### Fake Spawner 343 | 344 | The Go Expect package includes a Fake Spawner `func SpawnFake(b []Batcher, timeout time.Duration, opt ...Option) (*GExpect, <-chan error, error)`. 345 | This is expected to be used to simplify testing and faking of interactive workflows. 346 | 347 | *Fake Spawner* 348 | 349 | ``` 350 | // TestExpect tests the Expect function. 351 | func TestExpect(t *testing.T) { 352 | tests := []struct { 353 | name string 354 | fail bool 355 | srv []Batcher 356 | timeout time.Duration 357 | re *regexp.Regexp 358 | }{{ 359 | name: "Match prompt", 360 | srv: []Batcher{ 361 | &BSnd{` 362 | Pretty please don't hack my chassis 363 | 364 | router1> `}, 365 | }, 366 | re: regexp.MustCompile("router1>"), 367 | timeout: 2 * time.Second, 368 | }, { 369 | name: "Match fail", 370 | fail: true, 371 | re: regexp.MustCompile("router1>"), 372 | srv: []Batcher{ 373 | &BSnd{` 374 | Welcome 375 | 376 | Router42>`}, 377 | }, 378 | timeout: 1 * time.Second, 379 | }} 380 | 381 | for _, tst := range tests { 382 | exp, _, err := SpawnFake(tst.srv, tst.timeout) 383 | if err != nil { 384 | if !tst.fail { 385 | t.Errorf("%s: SpawnFake failed: %v", tst.name, err) 386 | } 387 | continue 388 | } 389 | out, _, err := exp.Expect(tst.re, tst.timeout) 390 | if got, want := err != nil, tst.fail; got != want { 391 | t.Errorf("%s: Expect(%q,%v) = %t want: %t , err: %v, out: %q", tst.name, tst.re.String(), tst.timeout, got, want, err, out) 392 | continue 393 | } 394 | } 395 | } 396 | ``` 397 | 398 | *Disclaimer: This is not an official Google product.* 399 | -------------------------------------------------------------------------------- /examples/newspawner/telnet.go: -------------------------------------------------------------------------------- 1 | // telnet creates a new Expect spawner for Telnet. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "time" 8 | 9 | expect "github.com/google/goexpect" 10 | 11 | "github.com/golang/glog" 12 | "github.com/google/goterm/term" 13 | "github.com/ziutek/telnet" 14 | ) 15 | 16 | const ( 17 | network = "tcp" 18 | address = "telehack.com:23" 19 | timeout = 10 * time.Second 20 | command = "geoip" 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | fmt.Println(term.Bluef("Telnet spawner example")) 27 | exp, _, err := telnetSpawn(address, timeout, expect.Verbose(true)) 28 | if err != nil { 29 | glog.Exitf("telnetSpawn(%q,%v) failed: %v", address, timeout, err) 30 | } 31 | 32 | defer func() { 33 | if err := exp.Close(); err != nil { 34 | glog.Infof("exp.Close failed: %v", err) 35 | } 36 | }() 37 | 38 | res, err := exp.ExpectBatch([]expect.Batcher{ 39 | &expect.BExp{R: `\n\.`}, 40 | &expect.BSnd{S: command + "\r\n"}, 41 | &expect.BExp{R: `\n\.`}, 42 | }, timeout) 43 | if err != nil { 44 | glog.Exitf("exp.ExpectBatch failed: %v , res: %v", err, res) 45 | } 46 | fmt.Println(term.Greenf("Res: %s", res[len(res)-1].Output)) 47 | 48 | } 49 | 50 | func telnetSpawn(addr string, timeout time.Duration, opts ...expect.Option) (expect.Expecter, <-chan error, error) { 51 | conn, err := telnet.Dial(network, addr) 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | 56 | resCh := make(chan error) 57 | 58 | return expect.SpawnGeneric(&expect.GenOptions{ 59 | In: conn, 60 | Out: conn, 61 | Wait: func() error { 62 | return <-resCh 63 | }, 64 | Close: func() error { 65 | close(resCh) 66 | return conn.Close() 67 | }, 68 | Check: func() bool { return true }, 69 | }, timeout, opts...) 70 | } 71 | -------------------------------------------------------------------------------- /examples/process/process.go: -------------------------------------------------------------------------------- 1 | // process is a simple example of spawning a process from the expect package. 2 | package main 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "time" 11 | 12 | "github.com/golang/glog" 13 | expect "github.com/google/goexpect" 14 | "github.com/google/goterm/term" 15 | ) 16 | 17 | const ( 18 | command = `bc -l` 19 | timeout = 10 * time.Minute 20 | ) 21 | 22 | var piRE = regexp.MustCompile(`3.14[0-9]*`) 23 | 24 | func main() { 25 | flag.Parse() 26 | if flag.NArg() != 1 { 27 | glog.Exitf("Usage: process ") 28 | } 29 | 30 | if err := os.Setenv("BC_LINE_LENGTH", "0"); err != nil { 31 | glog.Exit(err) 32 | } 33 | 34 | scale, err := strconv.Atoi(flag.Arg(0)) 35 | if err != nil { 36 | glog.Exit(err) 37 | } 38 | 39 | if scale < 3 { 40 | glog.Exitf("scale must be at least 3 for this sample to work") 41 | } 42 | 43 | e, _, err := expect.Spawn(command, -1) 44 | if err != nil { 45 | glog.Exit(err) 46 | } 47 | 48 | if err := e.Send("scale=" + strconv.Itoa(scale) + "\n"); err != nil { 49 | glog.Exit(err) 50 | } 51 | if err := e.Send("4*a(1)\n"); err != nil { 52 | glog.Exit(err) 53 | } 54 | out, match, err := e.Expect(piRE, timeout) 55 | if err != nil { 56 | glog.Exitf("e.Expect(%q,%v) failed: %v, out: %q", piRE.String(), timeout, err, out) 57 | } 58 | 59 | fmt.Println(term.Bluef("Pi with %d digits: %s", scale, match[0])) 60 | } 61 | -------------------------------------------------------------------------------- /expect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package expect is a Go version of the classic TCL Expect. 6 | package expect 7 | 8 | import ( 9 | "bytes" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "log" 14 | "os" 15 | "os/exec" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | "sync" 20 | "syscall" 21 | "time" 22 | 23 | "golang.org/x/crypto/ssh" 24 | "google.golang.org/grpc/codes" 25 | "google.golang.org/grpc/status" 26 | 27 | "github.com/google/goterm/term" 28 | ) 29 | 30 | // DefaultTimeout is the default Expect timeout. 31 | const DefaultTimeout = 60 * time.Second 32 | 33 | const ( 34 | checkDuration = 2 * time.Second // checkDuration how often to check for new output. 35 | defaultBufferSize = 8192 // defaultBufferSize is the default io buffer size. 36 | ) 37 | 38 | // Status contains an errormessage and a status code. 39 | type Status struct { 40 | code codes.Code 41 | msg string 42 | } 43 | 44 | // NewStatus creates a Status with the provided code and message. 45 | func NewStatus(code codes.Code, msg string) *Status { 46 | return &Status{code, msg} 47 | } 48 | 49 | // NewStatusf returns a Status with the provided code and a formatted message. 50 | func NewStatusf(code codes.Code, format string, a ...interface{}) *Status { 51 | return NewStatus(code, fmt.Sprintf(fmt.Sprintf(format, a...))) 52 | } 53 | 54 | // Err is a helper to handle errors. 55 | func (s *Status) Err() error { 56 | if s == nil || s.code == codes.OK { 57 | return nil 58 | } 59 | return s 60 | } 61 | 62 | // Error is here to adhere to the error interface. 63 | func (s *Status) Error() string { 64 | return s.msg 65 | } 66 | 67 | // Option represents one Expecter option. 68 | type Option func(*GExpect) Option 69 | 70 | // CheckDuration changes the default duration checking for new incoming data. 71 | func CheckDuration(d time.Duration) Option { 72 | return func(e *GExpect) Option { 73 | prev := e.chkDuration 74 | e.chkDuration = d 75 | return CheckDuration(prev) 76 | } 77 | } 78 | 79 | // SendTimeout set timeout for Send commands 80 | func SendTimeout(timeout time.Duration) Option { 81 | return func(e *GExpect) Option { 82 | prev := e.sendTimeout 83 | e.sendTimeout = timeout 84 | return SendTimeout(prev) 85 | } 86 | } 87 | 88 | // Verbose enables/disables verbose logging of matches and sends. 89 | func Verbose(v bool) Option { 90 | return func(e *GExpect) Option { 91 | prev := e.verbose 92 | e.verbose = v 93 | return Verbose(prev) 94 | } 95 | } 96 | 97 | // VerboseWriter sets an alternate destination for verbose logs. 98 | func VerboseWriter(w io.Writer) Option { 99 | return func(e *GExpect) Option { 100 | prev := e.verboseWriter 101 | e.verboseWriter = w 102 | return VerboseWriter(prev) 103 | } 104 | } 105 | 106 | // Tee duplicates all of the spawned process's output to the given writer and 107 | // closes the writer when complete. Writes occur from another thread, so 108 | // synchronization may be necessary. 109 | func Tee(w io.WriteCloser) Option { 110 | return func(e *GExpect) Option { 111 | prev := e.teeWriter 112 | e.teeWriter = w 113 | return Tee(prev) 114 | } 115 | } 116 | 117 | // NoCheck turns off the Expect alive checks. 118 | func NoCheck() Option { 119 | return changeChk(func(*GExpect) bool { 120 | return true 121 | }) 122 | } 123 | 124 | // DebugCheck adds logging to the check function. 125 | // The check function for the spawners are called at creation/timeouts and I/O so can 126 | // be usable for printing current state during debugging. 127 | func DebugCheck(l *log.Logger) Option { 128 | lg := log.Printf 129 | if l != nil { 130 | lg = l.Printf 131 | } 132 | return func(e *GExpect) Option { 133 | prev := e.chk 134 | e.chkMu.Lock() 135 | e.chk = func(ge *GExpect) bool { 136 | res := prev(ge) 137 | ge.mu.Lock() 138 | lg("chk: %t, ge: %v", res, ge) 139 | ge.mu.Unlock() 140 | return res 141 | } 142 | e.chkMu.Unlock() 143 | return changeChk(prev) 144 | } 145 | } 146 | 147 | // ChangeCheck changes the Expect check function. 148 | func ChangeCheck(f func() bool) Option { 149 | return changeChk(func(*GExpect) bool { 150 | return f() 151 | }) 152 | } 153 | 154 | func changeChk(f func(*GExpect) bool) Option { 155 | return func(e *GExpect) Option { 156 | prev := e.chk 157 | e.chkMu.Lock() 158 | e.chk = f 159 | e.chkMu.Unlock() 160 | return changeChk(prev) 161 | } 162 | } 163 | 164 | // SetEnv sets the environmental variables of the spawned process. 165 | func SetEnv(env []string) Option { 166 | return func(e *GExpect) Option { 167 | prev := e.cmd.Env 168 | e.cmd.Env = env 169 | return SetEnv(prev) 170 | } 171 | } 172 | 173 | // SetSysProcAttr sets the SysProcAttr syscall values for the spawned process. 174 | // Because this modifies cmd, it will only work with the process spawners 175 | // and not effect the GExpect option method. 176 | func SetSysProcAttr(args *syscall.SysProcAttr) Option { 177 | return func(e *GExpect) Option { 178 | prev := e.cmd.SysProcAttr 179 | e.cmd.SysProcAttr = args 180 | return SetSysProcAttr(prev) 181 | } 182 | } 183 | 184 | // PartialMatch enables/disables the returning of unmatched buffer so that consecutive expect call works. 185 | func PartialMatch(v bool) Option { 186 | return func(e *GExpect) Option { 187 | prev := e.partialMatch 188 | e.partialMatch = v 189 | return PartialMatch(prev) 190 | } 191 | } 192 | 193 | // BufferSize sets the size of receive buffer in bytes. 194 | func BufferSize(bufferSize int) Option { 195 | return func(e *GExpect) Option { 196 | e.bufferSizeIsSet = true 197 | prev := e.bufferSize 198 | e.bufferSize = bufferSize 199 | return BufferSize(prev) 200 | } 201 | } 202 | 203 | // BatchCommands. 204 | const ( 205 | // BatchSend for invoking Send in a batch 206 | BatchSend = iota 207 | // BatchExpect for invoking Expect in a batch 208 | BatchExpect 209 | // BatchSwitchCase for invoking ExpectSwitchCase in a batch 210 | BatchSwitchCase 211 | // BatchSendSignal for invoking SendSignal in a batch. 212 | BatchSendSignal 213 | ) 214 | 215 | // TimeoutError is the error returned by all Expect functions upon timer expiry. 216 | type TimeoutError int 217 | 218 | // Error implements the Error interface. 219 | func (t TimeoutError) Error() string { 220 | return fmt.Sprintf("expect: timer expired after %d seconds", time.Duration(t)/time.Second) 221 | } 222 | 223 | // BatchRes returned from ExpectBatch for every Expect command executed. 224 | type BatchRes struct { 225 | // Idx is used to match the result with the []Batcher commands sent in. 226 | Idx int 227 | // Out output buffer for the expect command at Batcher[Idx]. 228 | Output string 229 | // Match regexp matches for expect command at Batcher[Idx]. 230 | Match []string 231 | } 232 | 233 | // Batcher interface is used to make it more straightforward and readable to create 234 | // batches of Expects. 235 | // 236 | // var batch = []Batcher{ 237 | // &BExpT{"password",8}, 238 | // &BSnd{"password\n"}, 239 | // &BExp{"olakar@router>"}, 240 | // &BSnd{ "show interface description\n"}, 241 | // &BExp{ "olakar@router>"}, 242 | // } 243 | // 244 | // var batchSwCaseReplace = []Batcher{ 245 | // &BCasT{[]Caser{ 246 | // &BCase{`([0-9]) -- .*\(MASTER\)`, `\1` + "\n"}}, 1}, 247 | // &BExp{`prompt/>`}, 248 | // } 249 | type Batcher interface { 250 | // cmd returns the Batch command. 251 | Cmd() int 252 | // Arg returns the command argument. 253 | Arg() string 254 | // Timeout returns the timeout duration for the command , <0 gives default value. 255 | Timeout() time.Duration 256 | // Cases returns the Caser structure for SwitchCase commands. 257 | Cases() []Caser 258 | } 259 | 260 | // BSig implements the Batcher interface for SendSignal commands. 261 | type BSig struct { 262 | // S contains the signal. 263 | S syscall.Signal 264 | } 265 | 266 | // Cmd returns the SendSignal command (BatchSendSignal). 267 | func (bs *BSig) Cmd() int { 268 | return BatchSendSignal 269 | } 270 | 271 | // Arg returns the signal integer. 272 | func (bs *BSig) Arg() string { 273 | return strconv.Itoa(int(bs.S)) 274 | } 275 | 276 | // Timeout always returns 0 for BSig. 277 | func (bs *BSig) Timeout() time.Duration { 278 | return time.Duration(0) 279 | } 280 | 281 | // Cases always returns nil for BSig. 282 | func (bs *BSig) Cases() []Caser { 283 | return nil 284 | } 285 | 286 | // BExp implements the Batcher interface for Expect commands using the default timeout. 287 | type BExp struct { 288 | // R contains the Expect command regular expression. 289 | R string 290 | } 291 | 292 | // Cmd returns the Expect command (BatchExpect). 293 | func (be *BExp) Cmd() int { 294 | return BatchExpect 295 | } 296 | 297 | // Arg returns the Expect regular expression. 298 | func (be *BExp) Arg() string { 299 | return be.R 300 | } 301 | 302 | // Timeout always returns -1 which sets it to the value used to call the ExpectBatch function. 303 | func (be *BExp) Timeout() time.Duration { 304 | return -1 305 | } 306 | 307 | // Cases always returns nil for the Expect command. 308 | func (be *BExp) Cases() []Caser { 309 | return nil 310 | } 311 | 312 | // BExpT implements the Batcher interface for Expect commands adding a timeout option to the BExp 313 | // type. 314 | type BExpT struct { 315 | // R contains the Expect command regular expression. 316 | R string 317 | // T holds the Expect command timeout in seconds. 318 | T int 319 | } 320 | 321 | // Cmd returns the Expect command (BatchExpect). 322 | func (bt *BExpT) Cmd() int { 323 | return BatchExpect 324 | } 325 | 326 | // Timeout returns the timeout in seconds. 327 | func (bt *BExpT) Timeout() time.Duration { 328 | return time.Duration(bt.T) * time.Second 329 | } 330 | 331 | // Arg returns the Expect regular expression. 332 | func (bt *BExpT) Arg() string { 333 | return bt.R 334 | } 335 | 336 | // Cases always return nil for the Expect command. 337 | func (bt *BExpT) Cases() []Caser { 338 | return nil 339 | } 340 | 341 | // BSnd implements the Batcher interface for Send commands. 342 | type BSnd struct { 343 | S string 344 | } 345 | 346 | // Cmd returns the Send command(BatchSend). 347 | func (bs *BSnd) Cmd() int { 348 | return BatchSend 349 | } 350 | 351 | // Arg returns the data to be sent. 352 | func (bs *BSnd) Arg() string { 353 | return bs.S 354 | } 355 | 356 | // Timeout always returns 0 , Send doesn't have a timeout. 357 | func (bs *BSnd) Timeout() time.Duration { 358 | return 0 359 | } 360 | 361 | // Cases always returns nil , not used for Send commands. 362 | func (bs *BSnd) Cases() []Caser { 363 | return nil 364 | } 365 | 366 | // BCas implements the Batcher interface for SwitchCase commands. 367 | type BCas struct { 368 | // C holds the Caser array for the SwitchCase command. 369 | C []Caser 370 | } 371 | 372 | // Cmd returns the SwitchCase command(BatchSwitchCase). 373 | func (bc *BCas) Cmd() int { 374 | return BatchSwitchCase 375 | } 376 | 377 | // Arg returns an empty string , not used for SwitchCase. 378 | func (bc *BCas) Arg() string { 379 | return "" 380 | } 381 | 382 | // Timeout returns -1 , setting it to the default value. 383 | func (bc *BCas) Timeout() time.Duration { 384 | return -1 385 | } 386 | 387 | // Cases returns the Caser structure. 388 | func (bc *BCas) Cases() []Caser { 389 | return bc.C 390 | } 391 | 392 | // BCasT implements the Batcher interfacs for SwitchCase commands, adding a timeout option 393 | // to the BCas type. 394 | type BCasT struct { 395 | // Cs holds the Caser array for the SwitchCase command. 396 | C []Caser 397 | // Tout holds the SwitchCase timeout in seconds. 398 | T int 399 | } 400 | 401 | // Timeout returns the timeout in seconds. 402 | func (bct *BCasT) Timeout() time.Duration { 403 | return time.Duration(bct.T) * time.Second 404 | } 405 | 406 | // Cmd returns the SwitchCase command(BatchSwitchCase). 407 | func (bct *BCasT) Cmd() int { 408 | return BatchSwitchCase 409 | } 410 | 411 | // Arg returns an empty string , not used for SwitchCase. 412 | func (bct *BCasT) Arg() string { 413 | return "" 414 | } 415 | 416 | // Cases returns the Caser structure. 417 | func (bct *BCasT) Cases() []Caser { 418 | return bct.C 419 | } 420 | 421 | // Tag represents the state for a Caser. 422 | type Tag int32 423 | 424 | const ( 425 | // OKTag marks the desired state was reached. 426 | OKTag = Tag(iota) 427 | // FailTag means reaching this state will fail the Switch/Case. 428 | FailTag 429 | // ContinueTag will recheck for matches. 430 | ContinueTag 431 | // NextTag skips match and continues to the next one. 432 | NextTag 433 | // NoTag signals no tag was set for this case. 434 | NoTag 435 | ) 436 | 437 | // OK returns the OK Tag and status. 438 | func OK() func() (Tag, *Status) { 439 | return func() (Tag, *Status) { 440 | return OKTag, NewStatus(codes.OK, "state reached") 441 | } 442 | } 443 | 444 | // Fail returns Fail Tag and status. 445 | func Fail(s *Status) func() (Tag, *Status) { 446 | return func() (Tag, *Status) { 447 | return FailTag, s 448 | } 449 | } 450 | 451 | // Continue returns the Continue Tag and status. 452 | func Continue(s *Status) func() (Tag, *Status) { 453 | return func() (Tag, *Status) { 454 | return ContinueTag, s 455 | } 456 | } 457 | 458 | // Next returns the Next Tag and status. 459 | func Next() func() (Tag, *Status) { 460 | return func() (Tag, *Status) { 461 | return NextTag, NewStatus(codes.Unimplemented, "Next returns not implemented") 462 | } 463 | } 464 | 465 | // LogContinue logs the message and returns the Continue Tag and status. 466 | func LogContinue(msg string, s *Status) func() (Tag, *Status) { 467 | return func() (Tag, *Status) { 468 | log.Print(msg) 469 | return ContinueTag, s 470 | } 471 | } 472 | 473 | // Caser is an interface for ExpectSwitchCase and Batch to be able to handle 474 | // both the Case struct and the more script friendly BCase struct. 475 | type Caser interface { 476 | // RE returns a compiled regexp 477 | RE() (*regexp.Regexp, error) 478 | // Send returns the send string 479 | String() string 480 | // Tag returns the Tag. 481 | Tag() (Tag, *Status) 482 | // Retry returns true if there are retries left. 483 | Retry() bool 484 | } 485 | 486 | // Case used by the ExpectSwitchCase to take different Cases. 487 | // Implements the Caser interface. 488 | type Case struct { 489 | // R is the compiled regexp to match. 490 | R *regexp.Regexp 491 | // S is the string to send if Regexp matches. 492 | S string 493 | // T is the Tag for this Case. 494 | T func() (Tag, *Status) 495 | // Rt specifies number of times to retry, only used for cases tagged with Continue. 496 | Rt int 497 | } 498 | 499 | // Tag returns the tag for this case. 500 | func (c *Case) Tag() (Tag, *Status) { 501 | if c.T == nil { 502 | return NoTag, NewStatus(codes.OK, "no Tag set") 503 | } 504 | return c.T() 505 | } 506 | 507 | // RE returns the compiled regular expression. 508 | func (c *Case) RE() (*regexp.Regexp, error) { 509 | return c.R, nil 510 | } 511 | 512 | // Retry decrements the Retry counter and checks if there are any retries left. 513 | func (c *Case) Retry() bool { 514 | defer func() { c.Rt-- }() 515 | return c.Rt > 0 516 | } 517 | 518 | // Send returns the string to send if regexp matches 519 | func (c *Case) String() string { 520 | return c.S 521 | } 522 | 523 | // BCase with just a string is a bit more friendly to scripting. 524 | // Implements the Caser interface. 525 | type BCase struct { 526 | // R contains the string regular expression. 527 | R string 528 | // S contains the string to be sent if R matches. 529 | S string 530 | // T contains the Tag. 531 | T func() (Tag, *Status) 532 | // Rt contains the number of retries. 533 | Rt int 534 | } 535 | 536 | // RE returns the compiled regular expression. 537 | func (b *BCase) RE() (*regexp.Regexp, error) { 538 | if b.R == "" { 539 | return nil, nil 540 | } 541 | return regexp.Compile(b.R) 542 | } 543 | 544 | // Send returns the string to send. 545 | func (b *BCase) String() string { 546 | return b.S 547 | } 548 | 549 | // Tag returns the BCase Tag. 550 | func (b *BCase) Tag() (Tag, *Status) { 551 | if b.T == nil { 552 | return NoTag, NewStatus(codes.OK, "no Tag set") 553 | } 554 | return b.T() 555 | } 556 | 557 | // Retry decrements the Retry counter and checks if there are any retries left. 558 | func (b *BCase) Retry() bool { 559 | b.Rt-- 560 | return b.Rt > -1 561 | } 562 | 563 | // Expecter interface primarily to make testing easier. 564 | type Expecter interface { 565 | // Expect reads output from a spawned session and tries matching it with the provided regular expression. 566 | // It returns all output found until match. 567 | Expect(*regexp.Regexp, time.Duration) (string, []string, error) 568 | // ExpectBatch takes an array of BatchEntries and runs through them in order. For every Expect 569 | // command a BatchRes entry is created with output buffer and sub matches. 570 | // Failure of any of the batch commands will stop the execution, returning the results up to the 571 | // failure. 572 | ExpectBatch([]Batcher, time.Duration) ([]BatchRes, error) 573 | // ExpectSwitchCase makes it possible to Expect with multiple regular expressions and actions. Returns the 574 | // full output and submatches of the commands together with an index for the matching Case. 575 | ExpectSwitchCase([]Caser, time.Duration) (string, []string, int, error) 576 | // Send sends data into the spawned session. 577 | Send(string) error 578 | // Close closes the spawned session and files. 579 | Close() error 580 | } 581 | 582 | // GExpect implements the Expecter interface. 583 | type GExpect struct { 584 | // pty holds the virtual terminal used to interact with the spawned commands. 585 | pty *term.PTY 586 | // cmd contains the cmd information for the spawned process. 587 | cmd *exec.Cmd 588 | ssh *ssh.Session 589 | // snd is the channel used by the Send command to send data into the spawned command. 590 | snd chan string 591 | // rcv is used to signal the Expect commands that new data arrived. 592 | rcv chan struct{} 593 | // chkMu lock protecting the check function. 594 | chkMu sync.RWMutex 595 | // chk contains the function to check if the spawned command is alive. 596 | chk func(*GExpect) bool 597 | // cls contains the function to close spawned command. 598 | cls func(*GExpect) error 599 | // timeout contains the default timeout for a spawned command. 600 | timeout time.Duration 601 | // sendTimeout contains the default timeout for a send command. 602 | sendTimeout time.Duration 603 | // chkDuration contains the duration between checks for new incoming data. 604 | chkDuration time.Duration 605 | // verbose enables verbose logging. 606 | verbose bool 607 | // verboseWriter if set specifies where to write verbose information. 608 | verboseWriter io.Writer 609 | // teeWriter receives a duplicate of the spawned process's output when set. 610 | teeWriter io.WriteCloser 611 | // PartialMatch enables the returning of unmatched buffer so that consecutive expect call works. 612 | partialMatch bool 613 | // bufferSize is the size of the io buffers in bytes. 614 | bufferSize int 615 | // bufferSizeIsSet tracks whether the bufferSize was set for a given GExpect instance. 616 | bufferSizeIsSet bool 617 | 618 | // mu protects the output buffer. It must be held for any operations on out. 619 | mu sync.Mutex 620 | out bytes.Buffer 621 | } 622 | 623 | // String implements the stringer interface. 624 | func (e *GExpect) String() string { 625 | res := fmt.Sprintf("%p: ", e) 626 | if e.pty != nil { 627 | _, name := e.pty.PTSName() 628 | res += fmt.Sprintf("pty: %s ", name) 629 | } 630 | switch { 631 | case e.cmd != nil: 632 | res += fmt.Sprintf("cmd: %s(%d) ", e.cmd.Path, e.cmd.Process.Pid) 633 | case e.ssh != nil: 634 | res += fmt.Sprint("ssh session ") 635 | } 636 | res += fmt.Sprintf("buf: %q", e.out.String()) 637 | return res 638 | } 639 | 640 | // ExpectBatch takes an array of BatchEntry and executes them in order filling in the BatchRes 641 | // array for any Expect command executed. 642 | func (e *GExpect) ExpectBatch(batch []Batcher, timeout time.Duration) ([]BatchRes, error) { 643 | res := []BatchRes{} 644 | for i, b := range batch { 645 | switch b.Cmd() { 646 | case BatchExpect: 647 | re, err := regexp.Compile(b.Arg()) 648 | if err != nil { 649 | return res, err 650 | } 651 | to := b.Timeout() 652 | if to < 0 { 653 | to = timeout 654 | } 655 | out, match, err := e.Expect(re, to) 656 | res = append(res, BatchRes{i, out, match}) 657 | if err != nil { 658 | return res, err 659 | } 660 | case BatchSend: 661 | if err := e.Send(b.Arg()); err != nil { 662 | return res, err 663 | } 664 | case BatchSwitchCase: 665 | to := b.Timeout() 666 | if to < 0 { 667 | to = timeout 668 | } 669 | out, match, _, err := e.ExpectSwitchCase(b.Cases(), to) 670 | res = append(res, BatchRes{i, out, match}) 671 | if err != nil { 672 | return res, err 673 | } 674 | case BatchSendSignal: 675 | sigNr, err := strconv.Atoi(b.Arg()) 676 | if err != nil { 677 | return res, err 678 | } 679 | if err := e.SendSignal(syscall.Signal(sigNr)); err != nil { 680 | return res, err 681 | } 682 | default: 683 | return res, errors.New("unknown command:" + strconv.Itoa(b.Cmd())) 684 | } 685 | } 686 | return res, nil 687 | } 688 | 689 | func (e *GExpect) check() bool { 690 | e.chkMu.RLock() 691 | defer e.chkMu.RUnlock() 692 | return e.chk(e) 693 | } 694 | 695 | // SendSignal sends a signal to the Expect controlled process. 696 | // Only works on Process Expecters. 697 | func (e *GExpect) SendSignal(sig os.Signal) error { 698 | if e.cmd == nil { 699 | return status.Errorf(codes.Unimplemented, "only process Expecters supported") 700 | } 701 | return e.cmd.Process.Signal(sig) 702 | } 703 | 704 | // ExpectSwitchCase checks each Case against the accumulated out buffer, sending specified 705 | // string back. Leaving Send empty will Send nothing to the process. 706 | // Substring expansion can be used eg. 707 | // Case{`vf[0-9]{2}.[a-z]{3}[0-9]{2}\.net).*UP`,`show arp \1`} 708 | // Given: vf11.hnd01.net UP 35 (4) 34 (4) CONNECTED 0 0/0 709 | // Would send: show arp vf11.hnd01.net 710 | func (e *GExpect) ExpectSwitchCase(cs []Caser, timeout time.Duration) (string, []string, int, error) { 711 | // Compile all regexps 712 | rs := make([]*regexp.Regexp, 0, len(cs)) 713 | for _, c := range cs { 714 | re, err := c.RE() 715 | if err != nil { 716 | return "", []string{""}, -1, err 717 | } 718 | rs = append(rs, re) 719 | } 720 | // Setup timeouts 721 | // timeout == 0 => Just dump the buffer and exit. 722 | // timeout < 0 => Set default value. 723 | if timeout < 0 { 724 | timeout = e.timeout 725 | } 726 | timer := time.NewTimer(timeout) 727 | check := e.chkDuration 728 | // Check if any new data arrived every checkDuration interval. 729 | // If timeout/4 is less than the checkout interval we set the checkout to 730 | // timeout/4. If timeout ends up being 0 we bump it to one to keep the Ticker from 731 | // panicking. 732 | // All this b/c of the unreliable channel send setup in the read function,making it 733 | // possible for Expect* functions to miss the rcv signal. 734 | // 735 | // from read(): 736 | // // Ping Expect function 737 | // select { 738 | // case e.rcv <- struct{}{}: 739 | // default: 740 | // } 741 | // 742 | // A signal is only sent if any Expect function is running. Expect could miss it 743 | // while playing around with buffers and matching regular expressions. 744 | if timeout>>2 < check { 745 | check = timeout >> 2 746 | if check <= 0 { 747 | check = 1 748 | } 749 | } 750 | chTicker := time.NewTicker(check) 751 | defer chTicker.Stop() 752 | // Read in current data and start actively check for matches. 753 | var tbuf bytes.Buffer 754 | if _, err := io.Copy(&tbuf, e); err != nil { 755 | return tbuf.String(), nil, -1, fmt.Errorf("io.Copy failed: %v", err) 756 | } 757 | for { 758 | L1: 759 | for i, c := range cs { 760 | if rs[i] == nil { 761 | continue 762 | } 763 | match := rs[i].FindStringSubmatch(tbuf.String()) 764 | if match == nil { 765 | continue 766 | } 767 | 768 | t, s := c.Tag() 769 | if t == NextTag && !c.Retry() { 770 | continue 771 | } 772 | 773 | if e.verbose { 774 | if e.verboseWriter != nil { 775 | vStr := fmt.Sprintln(term.Green("Match for RE:").String() + fmt.Sprintf(" %q found: %q Buffer: %s", rs[i].String(), match, tbuf.String())) 776 | for n, bytesRead, err := 0, 0, error(nil); bytesRead < len(vStr); bytesRead += n { 777 | n, err = e.verboseWriter.Write([]byte(vStr)[n:]) 778 | if err != nil { 779 | log.Printf("Write to Verbose Writer failed: %v", err) 780 | break 781 | } 782 | } 783 | } else { 784 | log.Printf("Match for RE: %q found: %q Buffer: %q", rs[i].String(), match, tbuf.String()) 785 | } 786 | } 787 | 788 | tbufString := tbuf.String() 789 | o := tbufString 790 | 791 | if e.partialMatch { 792 | // Return the part of the buffer that is not matched by the regular expression so that the next expect call will be able to match it. 793 | matchIndex := rs[i].FindStringIndex(tbufString) 794 | o = tbufString[0:matchIndex[1]] 795 | e.returnUnmatchedSuffix(tbufString[matchIndex[1]:]) 796 | } 797 | 798 | tbuf.Reset() 799 | 800 | st := c.String() 801 | // Replace the submatches \[0-9]+ in the send string. 802 | if len(match) > 1 && len(st) > 0 { 803 | for i := 1; i < len(match); i++ { 804 | // \(submatch) will be expanded in the Send string. 805 | // To escape use \\(number). 806 | si := strconv.Itoa(i) 807 | r := strings.NewReplacer(`\\`+si, `\`+si, `\`+si, `\\`+si) 808 | st = r.Replace(st) 809 | st = strings.Replace(st, `\\`+si, match[i], -1) 810 | } 811 | } 812 | // Don't send anything if string is empty. 813 | if st != "" { 814 | if err := e.Send(st); err != nil { 815 | return o, match, i, fmt.Errorf("failed to send: %q err: %v", st, err) 816 | } 817 | } 818 | // Tag handling. 819 | switch t { 820 | case OKTag, FailTag, NoTag: 821 | return o, match, i, s.Err() 822 | case ContinueTag: 823 | if !c.Retry() { 824 | return o, match, i, s.Err() 825 | } 826 | break L1 827 | case NextTag: 828 | break L1 829 | default: 830 | s = NewStatusf(codes.Unknown, "Tag: %d unknown, err: %v", t, s) 831 | } 832 | return o, match, i, s.Err() 833 | } 834 | if !e.check() { 835 | nr, err := io.Copy(&tbuf, e) 836 | if err != nil { 837 | return tbuf.String(), nil, -1, fmt.Errorf("io.Copy failed: %v", err) 838 | } 839 | if nr == 0 { 840 | return tbuf.String(), nil, -1, errors.New("expect: Process not running") 841 | } 842 | } 843 | select { 844 | case <-timer.C: 845 | // Expect timeout. 846 | nr, err := io.Copy(&tbuf, e) 847 | if err != nil { 848 | return tbuf.String(), nil, -1, fmt.Errorf("io.Copy failed: %v", err) 849 | } 850 | // If we got no new data we return otherwise give it another chance to match. 851 | if nr == 0 { 852 | return tbuf.String(), nil, -1, TimeoutError(timeout) 853 | } 854 | timer = time.NewTimer(timeout) 855 | case <-chTicker.C: 856 | // Periodical timer to make sure data is handled in case the <-e.rcv channel 857 | // was missed. 858 | if _, err := io.Copy(&tbuf, e); err != nil { 859 | return tbuf.String(), nil, -1, fmt.Errorf("io.Copy failed: %v", err) 860 | } 861 | case <-e.rcv: 862 | // Data to fetch. 863 | nr, err := io.Copy(&tbuf, e) 864 | if err != nil { 865 | return tbuf.String(), nil, -1, fmt.Errorf("io.Copy failed: %v", err) 866 | } 867 | // timer shoud be reset when new output is available. 868 | if nr > 0 { 869 | timer = time.NewTimer(timeout) 870 | } 871 | } 872 | } 873 | } 874 | 875 | // GenOptions contains the options needed to set up a generic Spawner. 876 | type GenOptions struct { 877 | // In is where Expect Send messages will be written. 878 | In io.WriteCloser 879 | // Out will be read and matched by the expecter. 880 | Out io.Reader 881 | // Wait is used by expect to know when the session is over and cleanup of io Go routines should happen. 882 | Wait func() error 883 | // Close will be called as part of the expect Close, should normally include a Close of the In WriteCloser. 884 | Close func() error 885 | // Check is called everytime a Send or Expect function is called to makes sure the session is still running. 886 | Check func() bool 887 | } 888 | 889 | // SpawnGeneric is used to write generic Spawners. It returns an Expecter. The returned channel will give the return 890 | // status of the spawned session, in practice this means the return value of the provided Wait function. 891 | func SpawnGeneric(opt *GenOptions, timeout time.Duration, opts ...Option) (*GExpect, <-chan error, error) { 892 | switch { 893 | case opt == nil: 894 | return nil, nil, errors.New("GenOptions is ") 895 | case opt.In == nil: 896 | return nil, nil, errors.New("In can't be ") 897 | case opt.Out == nil: 898 | return nil, nil, errors.New("Out can't be ") 899 | case opt.Wait == nil: 900 | return nil, nil, errors.New("Wait can't be ") 901 | case opt.Close == nil: 902 | return nil, nil, errors.New("Close can't be ") 903 | case opt.Check == nil: 904 | return nil, nil, errors.New("Check can't be ") 905 | } 906 | if timeout < 1 { 907 | timeout = DefaultTimeout 908 | } 909 | e := &GExpect{ 910 | rcv: make(chan struct{}), 911 | snd: make(chan string), 912 | timeout: timeout, 913 | chkDuration: checkDuration, 914 | cls: func(e *GExpect) error { 915 | return opt.Close() 916 | }, 917 | chk: func(e *GExpect) bool { 918 | return opt.Check() 919 | }, 920 | } 921 | 922 | for _, o := range opts { 923 | o(e) 924 | } 925 | 926 | // Set the buffer size to the default if expect.BufferSize(...) is not utilized. 927 | if !e.bufferSizeIsSet { 928 | e.bufferSize = defaultBufferSize 929 | } 930 | 931 | errCh := make(chan error, 1) 932 | go e.waitForSession(errCh, opt.Wait, opt.In, opt.Out, nil) 933 | return e, errCh, nil 934 | } 935 | 936 | // SpawnFake spawns an expect.Batcher. 937 | func SpawnFake(b []Batcher, timeout time.Duration, opt ...Option) (*GExpect, <-chan error, error) { 938 | rr, rw := io.Pipe() 939 | wr, ww := io.Pipe() 940 | done := make(chan struct{}) 941 | srv, _, err := SpawnGeneric(&GenOptions{ 942 | In: ww, 943 | Out: rr, 944 | Wait: func() error { 945 | <-done 946 | return nil 947 | }, 948 | Close: func() error { 949 | return ww.Close() 950 | }, 951 | Check: func() bool { return true }, 952 | }, timeout, opt...) 953 | if err != nil { 954 | return nil, nil, err 955 | } 956 | // The Tee option should only affect the output not the batcher 957 | srv.teeWriter = nil 958 | 959 | go func() { 960 | res, err := srv.ExpectBatch(b, timeout) 961 | if err != nil { 962 | log.Printf("ExpectBatch(%v,%v) failed: %v, out: %v", b, timeout, err, res) 963 | } 964 | close(done) 965 | }() 966 | 967 | return SpawnGeneric(&GenOptions{ 968 | In: rw, 969 | Out: wr, 970 | Close: func() error { 971 | srv.Close() 972 | return rw.Close() 973 | }, 974 | Check: func() bool { return true }, 975 | Wait: func() error { 976 | <-done 977 | return nil 978 | }, 979 | }, timeout, opt...) 980 | } 981 | 982 | // SpawnWithArgs starts a new process and collects the output. The error 983 | // channel returns the result of the command Spawned when it finishes. 984 | // Arguments may contain spaces. 985 | func SpawnWithArgs(command []string, timeout time.Duration, opts ...Option) (*GExpect, <-chan error, error) { 986 | pty, err := term.OpenPTY() 987 | if err != nil { 988 | return nil, nil, err 989 | } 990 | var t term.Termios 991 | t.Raw() 992 | t.Set(pty.Slave) 993 | 994 | if timeout < 1 { 995 | timeout = DefaultTimeout 996 | } 997 | // Get the command up and running 998 | cmd := exec.Command(command[0], command[1:]...) 999 | // This ties the commands Stdin,Stdout & Stderr to the virtual terminal we created 1000 | cmd.Stdin, cmd.Stdout, cmd.Stderr = pty.Slave, pty.Slave, pty.Slave 1001 | // New process needs to be the process leader and control of a tty 1002 | cmd.SysProcAttr = &syscall.SysProcAttr{ 1003 | Setsid: true, 1004 | Setctty: true} 1005 | e := &GExpect{ 1006 | rcv: make(chan struct{}), 1007 | snd: make(chan string), 1008 | cmd: cmd, 1009 | timeout: timeout, 1010 | chkDuration: checkDuration, 1011 | pty: pty, 1012 | cls: func(e *GExpect) error { 1013 | if e.cmd != nil { 1014 | return e.cmd.Process.Kill() 1015 | } 1016 | return nil 1017 | }, 1018 | chk: func(e *GExpect) bool { 1019 | if e.cmd.Process == nil { 1020 | return false 1021 | } 1022 | // Sending Signal 0 to a process returns nil if process can take a signal , something else if not. 1023 | return e.cmd.Process.Signal(syscall.Signal(0)) == nil 1024 | }, 1025 | } 1026 | for _, o := range opts { 1027 | o(e) 1028 | } 1029 | 1030 | // Set the buffer size to the default if expect.BufferSize(...) is not utilized. 1031 | if !e.bufferSizeIsSet { 1032 | e.bufferSize = defaultBufferSize 1033 | } 1034 | 1035 | res := make(chan error, 1) 1036 | go e.runcmd(res) 1037 | // Wait until command started 1038 | return e, res, <-res 1039 | } 1040 | 1041 | // Spawn starts a new process and collects the output. The error channel 1042 | // returns the result of the command Spawned when it finishes. Arguments may 1043 | // not contain spaces. 1044 | func Spawn(command string, timeout time.Duration, opts ...Option) (*GExpect, <-chan error, error) { 1045 | return SpawnWithArgs(strings.Fields(command), timeout, opts...) 1046 | } 1047 | 1048 | // SpawnSSH starts an interactive SSH session,ties it to a PTY and collects the output. The returned channel sends the 1049 | // state of the SSH session after it finishes. 1050 | func SpawnSSH(sshClient *ssh.Client, timeout time.Duration, opts ...Option) (*GExpect, <-chan error, error) { 1051 | tios := term.Termios{} 1052 | tios.Raw() 1053 | tios.Wz.WsCol, tios.Wz.WsRow = sshTermWidth, sshTermHeight 1054 | return SpawnSSHPTY(sshClient, timeout, tios, opts...) 1055 | } 1056 | 1057 | const ( 1058 | sshTerm = "xterm" 1059 | sshTermWidth = 132 1060 | sshTermHeight = 43 1061 | ) 1062 | 1063 | // SpawnSSHPTY starts an interactive SSH session and ties it to a local PTY, optionally requests a remote PTY. 1064 | func SpawnSSHPTY(sshClient *ssh.Client, timeout time.Duration, term term.Termios, opts ...Option) (*GExpect, <-chan error, error) { 1065 | if sshClient == nil { 1066 | return nil, nil, errors.New("*ssh.Client is nil") 1067 | } 1068 | if timeout < 1 { 1069 | timeout = DefaultTimeout 1070 | } 1071 | // Setup interactive session 1072 | session, err := sshClient.NewSession() 1073 | if err != nil { 1074 | return nil, nil, err 1075 | } 1076 | e := &GExpect{ 1077 | rcv: make(chan struct{}), 1078 | snd: make(chan string), 1079 | chk: func(e *GExpect) bool { 1080 | if e.ssh == nil { 1081 | return false 1082 | } 1083 | _, err := e.ssh.SendRequest("dummy", false, nil) 1084 | return err == nil 1085 | }, 1086 | cls: func(e *GExpect) error { 1087 | if e.ssh != nil { 1088 | return e.ssh.Close() 1089 | } 1090 | return nil 1091 | }, 1092 | ssh: session, 1093 | timeout: timeout, 1094 | chkDuration: checkDuration, 1095 | } 1096 | for _, o := range opts { 1097 | o(e) 1098 | } 1099 | 1100 | // Set the buffer size to the default if expect.BufferSize(...) is not utilized. 1101 | if !e.bufferSizeIsSet { 1102 | e.bufferSize = defaultBufferSize 1103 | } 1104 | 1105 | if term.Wz.WsCol == 0 { 1106 | term.Wz.WsCol = sshTermWidth 1107 | } 1108 | if term.Wz.WsRow == 0 { 1109 | term.Wz.WsRow = sshTermHeight 1110 | } 1111 | if err := session.RequestPty(sshTerm, int(term.Wz.WsRow), int(term.Wz.WsCol), term.ToSSH()); err != nil { 1112 | return nil, nil, err 1113 | } 1114 | inPipe, err := session.StdinPipe() 1115 | if err != nil { 1116 | return nil, nil, err 1117 | } 1118 | outPipe, err := session.StdoutPipe() 1119 | if err != nil { 1120 | return nil, nil, err 1121 | } 1122 | errPipe, err := session.StderrPipe() 1123 | if err != nil { 1124 | return nil, nil, err 1125 | } 1126 | if err := session.Shell(); err != nil { 1127 | return nil, nil, err 1128 | } 1129 | // Shell started. 1130 | errCh := make(chan error, 1) 1131 | go e.waitForSession(errCh, session.Wait, inPipe, outPipe, errPipe) 1132 | return e, errCh, nil 1133 | } 1134 | 1135 | func (e *GExpect) waitForSession(r chan error, wait func() error, sIn io.WriteCloser, sOut io.Reader, sErr io.Reader) { 1136 | chDone := make(chan struct{}) 1137 | var wg sync.WaitGroup 1138 | wg.Add(1) 1139 | go func() { 1140 | defer wg.Done() 1141 | for { 1142 | select { 1143 | case <-chDone: 1144 | return 1145 | case sstr, ok := <-e.snd: 1146 | if !ok { 1147 | log.Printf("Send channel closed") 1148 | return 1149 | } 1150 | if _, err := sIn.Write([]byte(sstr)); err != nil || !e.check() { 1151 | log.Printf("Write failed: %v", err) 1152 | return 1153 | } 1154 | } 1155 | } 1156 | }() 1157 | rdr := func(out io.Reader) { 1158 | defer wg.Done() 1159 | buf := make([]byte, e.bufferSize) 1160 | for { 1161 | nr, err := out.Read(buf) 1162 | if err != nil || !e.check() { 1163 | if e.teeWriter != nil { 1164 | e.teeWriter.Close() 1165 | } 1166 | if err == io.EOF { 1167 | if e.verbose { 1168 | log.Printf("read closing down: %v", err) 1169 | } 1170 | return 1171 | } 1172 | return 1173 | } 1174 | // Tee output to writer 1175 | if e.teeWriter != nil { 1176 | e.teeWriter.Write(buf[:nr]) 1177 | } 1178 | // Add to buffer 1179 | e.mu.Lock() 1180 | e.out.Write(buf[:nr]) 1181 | e.mu.Unlock() 1182 | // Inform Expect (if it's currently running) that there's some new data to look through. 1183 | select { 1184 | case e.rcv <- struct{}{}: 1185 | default: 1186 | } 1187 | } 1188 | } 1189 | wg.Add(1) 1190 | go rdr(sOut) 1191 | if sErr != nil { 1192 | wg.Add(1) 1193 | go rdr(sErr) 1194 | } 1195 | err := wait() 1196 | close(chDone) 1197 | wg.Wait() 1198 | r <- err 1199 | } 1200 | 1201 | // Close closes the Spawned session. 1202 | func (e *GExpect) Close() error { 1203 | return e.cls(e) 1204 | } 1205 | 1206 | // Read implements the reader interface for the out buffer. 1207 | func (e *GExpect) Read(p []byte) (nr int, err error) { 1208 | e.mu.Lock() 1209 | defer e.mu.Unlock() 1210 | return e.out.Read(p) 1211 | } 1212 | 1213 | func (e *GExpect) returnUnmatchedSuffix(p string) { 1214 | e.mu.Lock() 1215 | defer e.mu.Unlock() 1216 | newBuffer := bytes.NewBufferString(p) 1217 | newBuffer.WriteString(e.out.String()) 1218 | e.out = *newBuffer 1219 | } 1220 | 1221 | // Send sends a string to spawned process. 1222 | func (e *GExpect) Send(in string) error { 1223 | if !e.check() { 1224 | return errors.New("expect: Process not running") 1225 | } 1226 | 1227 | if e.sendTimeout == 0 { 1228 | e.snd <- in 1229 | } else { 1230 | select { 1231 | case <-time.After(e.sendTimeout): 1232 | return fmt.Errorf("send to spawned process command reached the timeout %v", e.sendTimeout) 1233 | case e.snd <- in: 1234 | } 1235 | } 1236 | 1237 | if e.verbose { 1238 | if e.verboseWriter != nil { 1239 | vStr := fmt.Sprintln(term.Blue("Sent:").String() + fmt.Sprintf(" %q", in)) 1240 | _, err := e.verboseWriter.Write([]byte(vStr)) 1241 | if err != nil { 1242 | log.Printf("Write to Verbose Writer failed: %v", err) 1243 | } 1244 | } else { 1245 | log.Printf("Sent: %q", in) 1246 | } 1247 | } 1248 | 1249 | return nil 1250 | } 1251 | 1252 | // runcmd executes the command and Wait for the return value. 1253 | func (e *GExpect) runcmd(res chan error) { 1254 | if err := e.cmd.Start(); err != nil { 1255 | res <- err 1256 | return 1257 | } 1258 | // Moving the go read/write functions here makes sure the command is started before first checking if it's running. 1259 | clean := make(chan struct{}) 1260 | chDone := e.goIO(clean) 1261 | // Signal command started 1262 | res <- nil 1263 | cErr := e.cmd.Wait() 1264 | close(chDone) 1265 | e.pty.Slave.Close() 1266 | // make sure the read/send routines are done before closing the pty. 1267 | <-clean 1268 | res <- cErr 1269 | } 1270 | 1271 | // goIO starts the io handlers. 1272 | func (e *GExpect) goIO(clean chan struct{}) (done chan struct{}) { 1273 | done = make(chan struct{}) 1274 | var ptySync sync.WaitGroup 1275 | ptySync.Add(2) 1276 | go e.read(done, &ptySync) 1277 | go e.send(done, &ptySync) 1278 | go func() { 1279 | ptySync.Wait() 1280 | e.pty.Master.Close() 1281 | close(clean) 1282 | }() 1283 | return done 1284 | } 1285 | 1286 | // Expect reads spawned processes output looking for input regular expression. 1287 | // Timeout set to 0 makes Expect return the current buffer. 1288 | // Negative timeout value sets it to Default timeout. 1289 | func (e *GExpect) Expect(re *regexp.Regexp, timeout time.Duration) (string, []string, error) { 1290 | out, match, _, err := e.ExpectSwitchCase([]Caser{&Case{re, "", nil, 0}}, timeout) 1291 | return out, match, err 1292 | } 1293 | 1294 | // Options sets the specified options. 1295 | func (e *GExpect) Options(opts ...Option) (prev Option) { 1296 | for _, o := range opts { 1297 | prev = o(e) 1298 | } 1299 | return prev 1300 | } 1301 | 1302 | // read reads from the PTY master and forwards to active Expect function. 1303 | func (e *GExpect) read(done chan struct{}, ptySync *sync.WaitGroup) { 1304 | defer ptySync.Done() 1305 | buf := make([]byte, e.bufferSize) 1306 | for { 1307 | nr, err := e.pty.Master.Read(buf) 1308 | if err != nil && !e.check() { 1309 | if e.teeWriter != nil { 1310 | e.teeWriter.Close() 1311 | } 1312 | if err == io.EOF { 1313 | if e.verbose { 1314 | log.Printf("read closing down: %v", err) 1315 | } 1316 | return 1317 | } 1318 | return 1319 | } 1320 | // Tee output to writer 1321 | if e.teeWriter != nil { 1322 | e.teeWriter.Write(buf[:nr]) 1323 | } 1324 | // Add to buffer 1325 | e.mu.Lock() 1326 | e.out.Write(buf[:nr]) 1327 | e.mu.Unlock() 1328 | // Ping Expect function 1329 | select { 1330 | case e.rcv <- struct{}{}: 1331 | default: 1332 | } 1333 | } 1334 | } 1335 | 1336 | // send writes to the PTY master. 1337 | func (e *GExpect) send(done chan struct{}, ptySync *sync.WaitGroup) { 1338 | defer ptySync.Done() 1339 | for { 1340 | select { 1341 | case <-done: 1342 | return 1343 | case sstr, ok := <-e.snd: 1344 | if !ok { 1345 | return 1346 | } 1347 | if _, err := e.pty.Master.Write([]byte(sstr)); err != nil || !e.check() { 1348 | log.Printf("send failed: %v", err) 1349 | } 1350 | } 1351 | } 1352 | } 1353 | -------------------------------------------------------------------------------- /expect_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package expect 6 | 7 | import ( 8 | "bufio" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "net" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | "sync" 21 | "syscall" 22 | "testing" 23 | "time" 24 | 25 | stdlog "log" 26 | 27 | "google.golang.org/grpc/codes" 28 | 29 | log "github.com/golang/glog" 30 | term "github.com/google/goterm/term" 31 | 32 | "golang.org/x/crypto/ssh" 33 | ) 34 | 35 | var ( 36 | // eReg identifies the expect commands in the shell files. 37 | eReg = regexp.MustCompile(`^# e: (.*)$`) 38 | // sReg identifies the send commands in the shell files. 39 | sReg = regexp.MustCompile(`^# s: (.*)$`) 40 | // sReg identifies the sw/case commands in the shell files. 41 | cReg = regexp.MustCompile(`^# c: '(.*)' (.*)$`) 42 | ) 43 | 44 | const expTestData = "./testdata/" 45 | 46 | var privateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 47 | MIIEpgIBAAKCAQEAnUKOpnWUyxCGy9lyTLK3Crd5BXGG718wIgHzpCvKbopVbYu4 48 | xa9Fk1cjPHp2F/pPwXmIKJuTVpkm/VXcFfABH6cMeszaXqVBhqm6AA0E7y0K0oYZ 49 | GUMMm3sBLPV3ydUHECI2NnEXOCLGysKM6Ht2ZuGxCKdXpquRE1HdLUUIJep31gSO 50 | J4dyQRk12VYHrpTjz1Tzv9prf76vJqYmr6+axeH7I3/9KGnPe1vD0z8NhOwqQONz 51 | DMSjpbSYFiVyRbDVgSi9xq+BeFrASZuwkoHut3tzmIvQLcGIR+LoOeN9mDpXbseQ 52 | y84PXkduF8udrBCIBETekmK7kqi6hwLRCn9/twIDAQABAoIBAQCTeEOvQ5oJpvDR 53 | HpN56ymNCiqZ+TERLhFEAtKIRGxrppufw6O89bToC5HGeAxgReIey6nscqADWFFg 54 | xfBCPjO/i/Y+/fVVReEht+3teEgFRhbc/tVwhBjBgOLEV1hC09rwvTRbb0fX43zJ 55 | zRE4Pfb1WXWbaNngOQkttdoURqTyb+n8zgwx0AUsueSrYxTk1UTF+Jet7g23jRjd 56 | YCCx9qhHez5yif+1LZGIqJD0OKGHr9q+bbOZpy5dqjamuf9ulBvnZkKzcuHf0m/W 57 | Vhf9YI8kOQhPXfztTnZrN5Jg64gGuvJ0sEZucp5hOR4hYkLagOCaUuNIeHG7SsYU 58 | hChWCDphAoGBAM7nr4t714etJnuRE39FG+rylV5K3T2osr2Hwp7wSZfXHZUcnk2N 59 | KSDA9tzeFYX7QxUlc7qNwsLC2WkK97x1WbrNdO8Zn5lmBHfyIS7jxEGQY9htWrgr 60 | sjAaUg/JfHMu/lxNAigXAaGU2VzsTySgB3eWbfaAd/sImaxnVPBajOARAoGBAMKT 61 | NykNYl3zg1pIXGQlu3a0pTv2gRcBnnW7bUAM6b6tDdQZ+5cbu43MfAwhsGsMZ/HL 62 | gKQRJI952olrPa4dEiirxUKqfVVPLnDcUSu6uhvJFpzN2YVdMyPNWc3V9lfIztbu 63 | UvCvupnmeViG9qRoJgbMLIBLBN1oS5MKOL55oqtHAoGBAMX21XZe4qRVHlniQEZo 64 | aELPIe1bMf3Z2FMRfzw1aiSW1R4jiK9o3a4SEuDWuL893jxwXh9jnbJdXklsDgbK 65 | PTVHeZd/672I58Of7vH/SXr13SJp1wAaBt6RgGzMen92uja0E9kp0gy475RCIaNI 66 | XnykeMf+uU1+OBLFt3ZVHS8RAoGBAJr/BpvPK6LHzsTmi6LDY/gFovKHRQH8qiwC 67 | 595z6ueXl0J0iDQxRVCJqe9IDu7XbR3yDEGl3kfku69oHDRMuCBp5LNceIaykr4Y 68 | 4xhAoOxtXXP/jt1sBsboWDddz+TR8+LG6o8MjUr3i4Z3zJXe2RvlHTX9jJyK7ljt 69 | dZJV9r0VAoGBAI+yBh2oa5D66XztQ2pfKm/B03RYARR0iKvho6Ass1nM1YSvhflt 70 | CkGNYNP5Mwr/VpfTppl0JTl9++gtoAmDgVm19JUYiABhYNmbTq62STJb+LkEL+xK 71 | Jf5UkUJOE8Rf+A1vmI1igjVffSIRLTJC6zOX0JCZMIFKZhyTZsPOuFcm 72 | -----END RSA PRIVATE KEY-----`) 73 | 74 | // SSHServer represents one local SSH server. 75 | type SSHServer struct { 76 | batch []Batcher 77 | cfg *ssh.ServerConfig 78 | l net.Listener 79 | sync.Mutex 80 | opts []Option 81 | term *term.Termios 82 | } 83 | 84 | // New returns a new SSHServer configured for username/password. 85 | func NewSSHServer(user, pass string, b []Batcher, t *term.Termios, opts ...Option) *SSHServer { 86 | srv := &ssh.ServerConfig{ 87 | PasswordCallback: func(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) { 88 | if c.User() == user && string(password) == pass { 89 | return nil, nil 90 | } 91 | return nil, errors.New("password rejected for:" + c.User()) 92 | }, 93 | } 94 | k, _ := ssh.ParsePrivateKey(privateKey) 95 | srv.AddHostKey(k) 96 | if t == nil { 97 | t = &term.Termios{ 98 | Wz: term.Winsize{ 99 | WsCol: 132, 100 | WsRow: 43, 101 | }, 102 | } 103 | } 104 | return &SSHServer{cfg: srv, batch: b, term: t, opts: opts} 105 | } 106 | 107 | // Batcher replaces the batcher. 108 | func (s *SSHServer) Batcher(b []Batcher) { 109 | s.Lock() 110 | s.batch = b 111 | s.Unlock() 112 | } 113 | 114 | // Termios replaces the termios. 115 | func (s *SSHServer) Termios(t *term.Termios) { 116 | if t == nil { 117 | t = &term.Termios{ 118 | Wz: term.Winsize{ 119 | WsCol: 132, 120 | WsRow: 43, 121 | }, 122 | } 123 | } 124 | s.term = t 125 | } 126 | 127 | // Serve spins up the SSH server and returns the port used. 128 | func (s *SSHServer) Serve() (uint16, error) { 129 | l, err := net.Listen("tcp", "") 130 | if err != nil { 131 | return 0, err 132 | } 133 | s.l = l 134 | go func() { 135 | for { 136 | c, err := l.Accept() 137 | if err != nil { 138 | log.Errorf("Accept failed: %v", err) 139 | return 140 | } 141 | go s.runBatch(c) 142 | } 143 | }() 144 | _, port, err := net.SplitHostPort(l.Addr().String()) 145 | if err != nil { 146 | return 0, err 147 | } 148 | p, err := strconv.Atoi(port) 149 | if err != nil { 150 | return 0, err 151 | } 152 | return uint16(p), nil 153 | } 154 | 155 | // Close closes the SSH server write pipe. 156 | func (s *SSHServer) Close() error { 157 | return s.l.Close() 158 | } 159 | 160 | const testTimeout = 20 * time.Second 161 | 162 | // RFC 4254 Section 6.2. 163 | type ptyRequestMsg struct { 164 | Term string 165 | Columns uint32 166 | Rows uint32 167 | Width uint32 168 | Height uint32 169 | Modelist string 170 | } 171 | 172 | func (s *SSHServer) runBatch(conn net.Conn) { 173 | defer conn.Close() 174 | _, chs, rq, err := ssh.NewServerConn(conn, s.cfg) 175 | if err != nil { 176 | log.Errorf("ssh.NewServerConn failed: %v", err) 177 | return 178 | } 179 | 180 | go ssh.DiscardRequests(rq) 181 | 182 | for ch := range chs { 183 | switch ch.ChannelType() { 184 | case "session": 185 | sch, in, err := ch.Accept() 186 | if err != nil { 187 | log.Errorf("ch.Accept failed: %v", err) 188 | return 189 | } 190 | for sess := range in { 191 | switch sess.Type { 192 | case "dummy": 193 | if err := sess.Reply(true, nil); err != nil { 194 | log.Errorf("sess.Reply(%t,nil) failed: %v", true, err) 195 | } 196 | case "pty-req": 197 | ptyReq := ptyRequestMsg{} 198 | if err := ssh.Unmarshal(sess.Payload, &ptyReq); err != nil { 199 | if err := sess.Reply(false, nil); err != nil { 200 | log.Errorf("sess.Reply(%t,nil) failed: %v", false, err) 201 | } 202 | log.Errorf("ssh.Unmarshal of PTYRequest failed: %v", err) 203 | continue 204 | } 205 | if ptyReq.Columns != uint32(s.term.Wz.WsCol) || ptyReq.Rows != uint32(s.term.Wz.WsRow) { 206 | log.Errorf("PTY cols/rows: %d/%d want: %d/%d", ptyReq.Columns, ptyReq.Rows, s.term.Wz.WsCol, s.term.Wz.WsRow) 207 | if err := sess.Reply(false, nil); err != nil { 208 | log.Errorf("sess.Reply(%t,nil) failed: %v", false, err) 209 | } 210 | continue 211 | } 212 | 213 | if err := sess.Reply(true, nil); err != nil { 214 | log.Errorf("sess.Reply(%t,nil) failed: %v", true, err) 215 | } 216 | case "shell": 217 | log.Infof("Shell request coming in") 218 | resCh := make(chan error) 219 | defer close(resCh) 220 | if err := sess.Reply(true, nil); err != nil { 221 | log.Errorf("sess.Reply(%t,nil) failed: %v", true, err) 222 | } 223 | 224 | rIn, wIn := io.Pipe() 225 | rOut, wOut := io.Pipe() 226 | go io.Copy(sch, rIn) 227 | go io.Copy(wOut, sch) 228 | 229 | go func() { 230 | 231 | exp, _, err := SpawnGeneric(&GenOptions{ 232 | In: wIn, 233 | Out: rOut, 234 | Wait: func() error { 235 | return <-resCh 236 | }, 237 | Close: func() error { return wIn.Close() }, 238 | Check: func() bool { return true }, 239 | }, testTimeout*2, s.opts...) 240 | s.Lock() 241 | out, err := exp.ExpectBatch(s.batch, testTimeout*2) 242 | if err != nil { 243 | log.Errorf("exp.ExpectBatch(%v) failed: %v, res: %v", s.batch, err, out) 244 | } 245 | s.Unlock() 246 | }() 247 | default: 248 | sess.Reply(false, []byte(fmt.Sprint("session type not supported"))) 249 | } 250 | } 251 | default: 252 | ch.Reject(ssh.UnknownChannelType, "channel type not supported") 253 | } 254 | } 255 | } 256 | 257 | // Tc is an example of implementing custom tag functions. 258 | type Tc uint32 259 | 260 | func NewTc() (t Tc) { 261 | return 262 | } 263 | 264 | // Count works like ContinueLog with a counter. 265 | func (t Tc) Count(msg string, s *Status) func() (Tag, *Status) { 266 | return func() (Tag, *Status) { 267 | t++ 268 | log.Infof("%d: %s", t, msg) 269 | return ContinueTag, s 270 | } 271 | } 272 | 273 | // NextLog adds loggin and counting to the Next tag. 274 | func (t Tc) NextLog(msg string) func() (Tag, *Status) { 275 | return func() (Tag, *Status) { 276 | t++ 277 | log.Infof("Next %d: %s", t, msg) 278 | return NextTag, NewStatus(codes.Unimplemented, "Should not matter") 279 | } 280 | } 281 | 282 | func TestBatcher(t *testing.T) { 283 | tests := []struct { 284 | name string 285 | clt, srv []Batcher 286 | fail bool 287 | }{ 288 | { 289 | name: "Config mode", 290 | clt: []Batcher{ 291 | &BExp{`router1>`}, 292 | &BSnd{"conf t\n"}, 293 | &BExp{`\(configure\) router1>`}, 294 | }, 295 | srv: []Batcher{ 296 | &BSnd{`router1> `}, 297 | &BExp{"conf t\n"}, 298 | &BSnd{`(configure) router1> `}, 299 | }}, { 300 | name: "Login caser", 301 | clt: []Batcher{ 302 | &BCas{[]Caser{ 303 | &Case{R: regexp.MustCompile(`Login: `), S: "TestUser\n", T: LogContinue("at login prompt", NewStatus(codes.PermissionDenied, "wrong username")), Rt: 1}, 304 | &Case{R: regexp.MustCompile(`Password: `), S: "TestPass\n", T: LogContinue("at password prompt", NewStatus(codes.PermissionDenied, "wrong pass")), Rt: 1}, 305 | &Case{R: regexp.MustCompile(`Permission denied`), T: Fail(NewStatus(codes.PermissionDenied, "login failed"))}, 306 | &Case{R: regexp.MustCompile(`router 1>`), T: OK()}, 307 | }}, 308 | }, 309 | srv: []Batcher{ 310 | &BSnd{"Login: "}, 311 | &BCas{[]Caser{ 312 | &Case{R: regexp.MustCompile("TestUser\n"), S: `Password: `, T: Continue(NewStatus(codes.PermissionDenied, "permission denied")), Rt: 3}, 313 | &Case{R: regexp.MustCompile("TestPass\n"), S: `router 1> `, T: OK()}, 314 | }, 315 | }, 316 | }}, { 317 | name: "100 Hello World", 318 | clt: []Batcher{ 319 | &BSnd{`Hello `}, 320 | &BCas{[]Caser{ 321 | &Case{R: regexp.MustCompile("Done"), T: OK()}, 322 | &Case{R: regexp.MustCompile("World"), S: "Hello ", T: NewTc().Count("Hello", NewStatus(codes.OutOfRange, "too many worlds")), Rt: 100}, 323 | }}, 324 | }, 325 | srv: []Batcher{ 326 | &BCas{[]Caser{ 327 | &Case{R: regexp.MustCompile("Hello"), S: "World\n", T: NewTc().Count("World", NewStatus(codes.OK, "too many hellos")), Rt: 99}, 328 | }}, 329 | &BSnd{"Done"}, 330 | }, 331 | }, { 332 | name: "100 Hello World using Next tag", 333 | clt: []Batcher{ 334 | &BSnd{`Hello `}, 335 | &BCas{[]Caser{ 336 | &Case{R: regexp.MustCompile("Done"), T: OK()}, 337 | &Case{R: regexp.MustCompile("World"), S: "Hello ", T: NewTc().Count("Hello", NewStatus(codes.OutOfRange, "too many worlds")), Rt: 100}, 338 | }}, 339 | }, 340 | srv: []Batcher{ 341 | &BCas{[]Caser{ 342 | &Case{R: regexp.MustCompile("Hello"), S: "World\n", T: NewTc().NextLog("World"), Rt: 99}, 343 | &Case{R: regexp.MustCompile("Hello"), S: "Done\n", T: LogContinue("Done sent", NewStatus(codes.OK, "Done sent"))}, 344 | }}, 345 | }, 346 | }, 347 | } 348 | 349 | srv := NewSSHServer("test", "test", nil, nil, CheckDuration(40*time.Millisecond)) 350 | port, err := srv.Serve() 351 | if err != nil { 352 | t.Fatalf("srv.Serve failed: %v", err) 353 | } 354 | defer srv.Close() 355 | for _, tst := range tests { 356 | srv.Batcher(tst.srv) 357 | clt, err := ssh.Dial("tcp", net.JoinHostPort("localhost", strconv.Itoa(int(port))), 358 | &ssh.ClientConfig{ 359 | User: "test", 360 | Auth: []ssh.AuthMethod{ssh.Password("test")}, 361 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 362 | }) 363 | if err != nil { 364 | t.Errorf("%s: ssh.Dial failed: %v", tst.name, err) 365 | continue 366 | } 367 | e, _, err := SpawnSSH(clt, testTimeout*2) 368 | if err != nil { 369 | t.Errorf("%s: SpawnSSH failed: %v", tst.name, err) 370 | } 371 | res, err := e.ExpectBatch(tst.clt, testTimeout*2) 372 | if got, want := err != nil, tst.fail; got != want { 373 | t.Errorf("%s: e.ExpectBatch(%v,_) = %v want: %v, res: %q", tst.name, tst.clt, err, want, res) 374 | } 375 | if err := clt.Close(); err != nil { 376 | t.Errorf("%s: clt.Close failed: %v", tst.name, err) 377 | } 378 | } 379 | } 380 | 381 | var ( 382 | cliMap = map[string]string{ 383 | "show system uptime": `Current time: 1998-10-13 19:45:47 UTC 384 | Time Source: NTP CLOCK 385 | System booted: 1998-10-12 20:51:41 UTC (22:54:06 ago) 386 | Protocols started: 1998-10-13 19:33:45 UTC (00:12:02 ago) 387 | Last configured: 1998-10-13 19:33:45 UTC (00:12:02 ago) by abc 388 | 12:45PM up 22:54, 2 users, load averages: 0.07, 0.02, 0.01 389 | 390 | testuser@testrouter#`, 391 | "show version": `Cisco IOS Software, 3600 Software (C3660-I-M), Version 12.3(4)T 392 | 393 | TAC Support: http://www.cisco.com/tac 394 | Copyright (c) 1986-2003 by Cisco Systems, Inc. 395 | Compiled Thu 18-Sep-03 15:37 by ccai 396 | 397 | ROM: System Bootstrap, Version 12.0(6r)T, RELEASE SOFTWARE (fc1) 398 | ROM: 399 | 400 | C3660-1 uptime is 1 week, 3 days, 6 hours, 41 minutes 401 | System returned to ROM by power-on 402 | System image file is "slot0:tftpboot/c3660-i-mz.123-4.T" 403 | 404 | Cisco 3660 (R527x) processor (revision 1.0) with 57344K/8192K bytes of memory. 405 | Processor board ID JAB055180FF 406 | R527x CPU at 225Mhz, Implementation 40, Rev 10.0, 2048KB L2 Cache 407 | 408 | 3660 Chassis type: ENTERPRISE 409 | 2 FastEthernet interfaces 410 | 4 Serial interfaces 411 | DRAM configuration is 64 bits wide with parity disabled. 412 | 125K bytes of NVRAM. 413 | 16384K bytes of processor board System flash (Read/Write) 414 | 415 | Flash card inserted. Reading filesystem...done. 416 | 20480K bytes of processor board PCMCIA Slot0 flash (Read/Write) 417 | 418 | Configuration register is 0x2102 419 | 420 | testrouter#`, 421 | "show system users": `7:30PM up 4 days, 2:26, 2 users, load averages: 0.07, 0.02, 0.01 422 | USER TTY FROM LOGIN@ IDLE WHAT 423 | root d0 - Fri05PM 4days -csh (csh) 424 | blue p0 level5.company.net 7:30PM - cli 425 | 426 | testuser@testrouter#`, 427 | } 428 | ) 429 | 430 | func fakeCli(tMap map[string]string, in io.Reader, out io.Writer) { 431 | scn := bufio.NewScanner(in) 432 | for scn.Scan() { 433 | tst, ok := tMap[scn.Text()] 434 | if !ok { 435 | out.Write([]byte(fmt.Sprintf("command: %q not found", scn.Text()))) 436 | continue 437 | } 438 | _, err := out.Write([]byte(tst)) 439 | if err != nil { 440 | log.Warningf("Write of %q failed: %v", tst, err) 441 | return 442 | } 443 | } 444 | } 445 | 446 | // ExampleDebugCheck toggles the DebugCheck option. 447 | func ExampleDebugCheck() { 448 | rIn, wIn := io.Pipe() 449 | rOut, wOut := io.Pipe() 450 | rLog, wLog := io.Pipe() 451 | waitCh := make(chan error) 452 | defer rIn.Close() 453 | defer wOut.Close() 454 | defer wLog.Close() 455 | 456 | go fakeCli(cliMap, rIn, wOut) 457 | 458 | exp, r, err := SpawnGeneric(&GenOptions{ 459 | In: wIn, 460 | Out: rOut, 461 | Wait: func() error { return <-waitCh }, 462 | Close: func() error { return wIn.Close() }, 463 | Check: func() bool { 464 | return true 465 | }}, -1) 466 | if err != nil { 467 | log.Errorf("SpawnGeneric failed: %v", err) 468 | return 469 | } 470 | re := regexp.MustCompile("testrouter#") 471 | interact := func() { 472 | for cmd := range cliMap { 473 | if err := exp.Send(cmd + "\n"); err != nil { 474 | log.Errorf("exp.Send(%q) failed: %v\n", cmd+"\n", err) 475 | return 476 | } 477 | out, _, err := exp.Expect(re, -1) 478 | if err != nil { 479 | log.Errorf("exp.Expect(%v) failed: %v out: %v", re, err, out) 480 | return 481 | } 482 | } 483 | } 484 | 485 | go func() { 486 | var last string 487 | scn := bufio.NewScanner(rLog) 488 | for scn.Scan() { 489 | ws := strings.Split(scn.Text(), " ") 490 | if ws[0] == last { 491 | continue 492 | } 493 | last = ws[0] 494 | fmt.Println(ws[0]) 495 | } 496 | }() 497 | 498 | fmt.Println("First round") 499 | interact() 500 | fmt.Println("Second round - Debugging enabled") 501 | prev := exp.Options(DebugCheck(stdlog.New(wLog, "DebugExample ", 0))) 502 | interact() 503 | exp.Options(prev) 504 | fmt.Println("Last round - Previous Check put back") 505 | interact() 506 | 507 | waitCh <- nil 508 | exp.Close() 509 | wOut.Close() 510 | 511 | <-r 512 | 513 | // Output: 514 | // First round 515 | // Second round - Debugging enabled 516 | // DebugExample 517 | // Last round - Previous Check put back 518 | } 519 | 520 | // ExampleChangeCheck changes the check function runtime for an Expect session. 521 | func ExampleChangeCheck() { 522 | rIn, wIn := io.Pipe() 523 | rOut, wOut := io.Pipe() 524 | waitCh := make(chan error) 525 | outCh := make(chan string) 526 | defer close(outCh) 527 | 528 | go fakeCli(cliMap, rIn, wOut) 529 | go func() { 530 | var last string 531 | for s := range outCh { 532 | if s == last { 533 | continue 534 | } 535 | fmt.Println(s) 536 | last = s 537 | } 538 | }() 539 | 540 | exp, r, err := SpawnGeneric(&GenOptions{ 541 | In: wIn, 542 | Out: rOut, 543 | Wait: func() error { return <-waitCh }, 544 | Close: func() error { return wIn.Close() }, 545 | Check: func() bool { 546 | outCh <- "Original check" 547 | return true 548 | }}, -1) 549 | if err != nil { 550 | fmt.Printf("SpawnGeneric failed: %v\n", err) 551 | return 552 | } 553 | re := regexp.MustCompile("testrouter#") 554 | interact := func() { 555 | for cmd := range cliMap { 556 | if err := exp.Send(cmd + "\n"); err != nil { 557 | fmt.Printf("exp.Send(%q) failed: %v\n", cmd+"\n", err) 558 | return 559 | } 560 | out, _, err := exp.Expect(re, -1) 561 | if err != nil { 562 | fmt.Printf("exp.Expect(%v) failed: %v out: %v", re, err, out) 563 | return 564 | } 565 | } 566 | } 567 | interact() 568 | prev := exp.Options(ChangeCheck(func() bool { 569 | outCh <- "Replaced check" 570 | return true 571 | })) 572 | interact() 573 | exp.Options(prev) 574 | interact() 575 | 576 | waitCh <- nil 577 | exp.Close() 578 | wOut.Close() 579 | 580 | <-r 581 | // Output: 582 | // Original check 583 | // Replaced check 584 | // Original check 585 | } 586 | 587 | // ExampleVerbose changes the Verbose and VerboseWriter options. 588 | func ExampleVerbose() { 589 | rIn, wIn := io.Pipe() 590 | rOut, wOut := io.Pipe() 591 | waitCh := make(chan error) 592 | outCh := make(chan string) 593 | defer close(outCh) 594 | 595 | go fakeCli(cliMap, rIn, wOut) 596 | go func() { 597 | var last string 598 | for s := range outCh { 599 | if s == last { 600 | continue 601 | } 602 | fmt.Println(s) 603 | last = s 604 | } 605 | }() 606 | 607 | exp, r, err := SpawnGeneric(&GenOptions{ 608 | In: wIn, 609 | Out: rOut, 610 | Wait: func() error { return <-waitCh }, 611 | Close: func() error { return wIn.Close() }, 612 | Check: func() bool { 613 | return true 614 | }}, -1, Verbose(true), VerboseWriter(os.Stdout)) 615 | if err != nil { 616 | fmt.Printf("SpawnGeneric failed: %v\n", err) 617 | return 618 | } 619 | re := regexp.MustCompile("testrouter#") 620 | var interactCmdSorted []string 621 | for k := range cliMap { 622 | interactCmdSorted = append(interactCmdSorted, k) 623 | } 624 | sort.Strings(interactCmdSorted) 625 | interact := func() { 626 | for _, cmd := range interactCmdSorted { 627 | if err := exp.Send(cmd + "\n"); err != nil { 628 | fmt.Printf("exp.Send(%q) failed: %v\n", cmd+"\n", err) 629 | return 630 | } 631 | out, _, err := exp.Expect(re, -1) 632 | if err != nil { 633 | fmt.Printf("exp.Expect(%v) failed: %v out: %v", re, err, out) 634 | return 635 | } 636 | } 637 | } 638 | interact() 639 | 640 | waitCh <- nil 641 | exp.Close() 642 | wOut.Close() 643 | 644 | <-r 645 | // Output: 646 | // Sent: "show system uptime\n" 647 | // Match for RE: "testrouter#" found: ["testrouter#"] Buffer: Current time: 1998-10-13 19:45:47 UTC 648 | // Time Source: NTP CLOCK 649 | // System booted: 1998-10-12 20:51:41 UTC (22:54:06 ago) 650 | // Protocols started: 1998-10-13 19:33:45 UTC (00:12:02 ago) 651 | // Last configured: 1998-10-13 19:33:45 UTC (00:12:02 ago) by abc 652 | // 12:45PM up 22:54, 2 users, load averages: 0.07, 0.02, 0.01 653 | // 654 | // testuser@testrouter# 655 | // Sent: "show system users\n" 656 | // Match for RE: "testrouter#" found: ["testrouter#"] Buffer: 7:30PM up 4 days, 2:26, 2 users, load averages: 0.07, 0.02, 0.01 657 | // USER TTY FROM LOGIN@ IDLE WHAT 658 | // root d0 - Fri05PM 4days -csh (csh) 659 | // blue p0 level5.company.net 7:30PM - cli 660 | // 661 | // testuser@testrouter# 662 | // Sent: "show version\n" 663 | // Match for RE: "testrouter#" found: ["testrouter#"] Buffer: Cisco IOS Software, 3600 Software (C3660-I-M), Version 12.3(4)T 664 | // 665 | // TAC Support: http://www.cisco.com/tac 666 | // Copyright (c) 1986-2003 by Cisco Systems, Inc. 667 | // Compiled Thu 18-Sep-03 15:37 by ccai 668 | // 669 | // ROM: System Bootstrap, Version 12.0(6r)T, RELEASE SOFTWARE (fc1) 670 | // ROM: 671 | // 672 | // C3660-1 uptime is 1 week, 3 days, 6 hours, 41 minutes 673 | // System returned to ROM by power-on 674 | // System image file is "slot0:tftpboot/c3660-i-mz.123-4.T" 675 | // 676 | // Cisco 3660 (R527x) processor (revision 1.0) with 57344K/8192K bytes of memory. 677 | // Processor board ID JAB055180FF 678 | // R527x CPU at 225Mhz, Implementation 40, Rev 10.0, 2048KB L2 Cache 679 | // 680 | // 3660 Chassis type: ENTERPRISE 681 | // 2 FastEthernet interfaces 682 | // 4 Serial interfaces 683 | // DRAM configuration is 64 bits wide with parity disabled. 684 | // 125K bytes of NVRAM. 685 | // 16384K bytes of processor board System flash (Read/Write) 686 | // 687 | // Flash card inserted. Reading filesystem...done. 688 | // 20480K bytes of processor board PCMCIA Slot0 flash (Read/Write) 689 | // 690 | // Configuration register is 0x2102 691 | // 692 | // testrouter# 693 | 694 | } 695 | 696 | // TestTee tests the Tee option can write to a file. 697 | func TestTee(t *testing.T) { 698 | // Create a temporary file to tee output. 699 | f, err := ioutil.TempFile("", "goexpect-tee") 700 | if err != nil { 701 | t.Fatalf("Could not create temporary file: %v", err) 702 | } 703 | fileName := f.Name() 704 | defer os.Remove(fileName) 705 | 706 | // Send abcdef to cat 4096 times. 707 | input := "abcdef\n" 708 | e, _, err := Spawn("cat", 400*time.Millisecond, Tee(f), CheckDuration(1*time.Millisecond)) 709 | for i := 0; i < 4096; i++ { 710 | e.Send(input) 711 | re := regexp.MustCompile(input) 712 | if _, _, err = e.Expect(re, 400*time.Millisecond); err != nil { 713 | t.Errorf("Expect(%q) failed: %v", input, err) 714 | } 715 | } 716 | e.Close() 717 | 718 | // Check the tee'd output. 719 | got, err := ioutil.ReadFile(fileName) 720 | if err != nil { 721 | t.Fatalf("Could not read temporary file: %v", err) 722 | } 723 | want := strings.Repeat(input, 4096) 724 | if string(got) != want { 725 | t.Errorf("tee output mismatch, got: %q want: %q", got, want) 726 | } 727 | } 728 | 729 | // TestTee_SpawnFake tests the Tee option can operate on SpawnFake. 730 | func TestTee_SpawnFake(t *testing.T) { 731 | // Create a temporary file to tee output. 732 | f, err := ioutil.TempFile("", "goexpect-tee") 733 | if err != nil { 734 | t.Fatalf("Could not create temporary file: %v", err) 735 | } 736 | fileName := f.Name() 737 | defer os.Remove(fileName) 738 | 739 | msg := ` 740 | Pretty please don't hack my chassis 741 | 742 | router1> ` 743 | srv := []Batcher{ 744 | &BSnd{msg}, 745 | } 746 | re := regexp.MustCompile("router1>") 747 | timeout := 2 * time.Second 748 | exp, endch, err := SpawnFake(srv, timeout, Tee(f)) 749 | if err != nil { 750 | t.Fatalf("SpawnFake failed: %v", err) 751 | } 752 | out, _, err := exp.Expect(re, timeout) 753 | if err != nil { 754 | t.Fatalf("Expect(%q,%v), err: %v, out: %q", re.String(), timeout, err, out) 755 | } 756 | exp.Close() 757 | // wait for end 758 | <-endch 759 | 760 | // Check the tee'd output. 761 | got, err := ioutil.ReadFile(fileName) 762 | if err != nil { 763 | t.Fatalf("Could not read temporary file: %v", err) 764 | } 765 | if string(got) != msg { 766 | t.Errorf("tee output mismatch, got: %q want: %q", got, msg) 767 | } 768 | } 769 | 770 | // TestSpawnGeneric tests out the generic spawn function. 771 | func TestSpawnGeneric(t *testing.T) { 772 | fr, fw := io.Pipe() 773 | tests := []struct { 774 | name string 775 | opt *GenOptions 776 | check func() bool 777 | cli map[string]string 778 | re *regexp.Regexp 779 | fail bool 780 | }{{ 781 | name: "Clean test", 782 | check: func() bool { return true }, 783 | cli: cliMap, 784 | re: regexp.MustCompile("testrouter#"), 785 | fail: false, 786 | }, { 787 | name: "Fail check", 788 | check: func() bool { 789 | return false 790 | }, 791 | cli: cliMap, 792 | re: regexp.MustCompile("testrouter#"), 793 | fail: true, 794 | }, { 795 | name: "In nil", 796 | opt: &GenOptions{}, 797 | fail: true, 798 | }, { 799 | name: "Out nil", 800 | opt: &GenOptions{ 801 | In: fw, 802 | }, 803 | fail: true, 804 | }, { 805 | name: "Wait nil", 806 | opt: &GenOptions{ 807 | In: fw, 808 | Out: fr, 809 | }, 810 | fail: true, 811 | }} 812 | 813 | for _, tst := range tests { 814 | t.Logf("Running test: %v", tst.name) 815 | waitCh := make(chan error) 816 | rIn, wIn := io.Pipe() 817 | rOut, wOut := io.Pipe() 818 | if tst.opt == nil { 819 | tst.opt = &GenOptions{ 820 | In: wIn, 821 | Out: rOut, 822 | Wait: func() error { return <-waitCh }, 823 | Close: func() error { return wIn.Close() }, 824 | Check: tst.check} 825 | } 826 | go fakeCli(tst.cli, rIn, wOut) 827 | exp, r, err := SpawnGeneric(tst.opt, -1) 828 | if err != nil { 829 | if !tst.fail { 830 | t.Errorf("test: %v , SpawnGeneric failed: %v", tst.name, err) 831 | } 832 | continue 833 | } 834 | gotFail := false 835 | for cmd := range tst.cli { 836 | err := exp.Send(cmd + "\n") 837 | if err != nil { 838 | if tst.fail { 839 | gotFail = true 840 | break 841 | } 842 | t.Errorf("Send(%q) failed: %v", cmd, err) 843 | break 844 | } 845 | out, _, err := exp.Expect(tst.re, -1) 846 | if err != nil { 847 | if tst.fail { 848 | gotFail = true 849 | break 850 | } 851 | t.Errorf("Expect(%q) failed: %v, out: %q", tst.re, err, out) 852 | break 853 | } 854 | } 855 | if gotFail != tst.fail { 856 | t.Errorf("test: %v , failed status mismatch, got: %v want: %v", tst.name, gotFail, tst.fail) 857 | } 858 | waitCh <- nil 859 | exp.Close() 860 | wOut.Close() 861 | 862 | <-r 863 | } 864 | } 865 | 866 | // TestSendTimeout tests that Send command can fail on timeout. 867 | func TestSendTimeout(t *testing.T) { 868 | t.Log("Running test: TestSendTimeout") 869 | rIn, wIn := io.Pipe() 870 | rOut, wOut := io.Pipe() 871 | waitCh := make(chan error) 872 | outCh := make(chan string) 873 | defer close(outCh) 874 | 875 | go fakeCli(cliMap, rIn, wOut) 876 | exp, r, err := SpawnGeneric( 877 | &GenOptions{ 878 | In: wIn, 879 | Out: rOut, 880 | Wait: func() error { return <-waitCh }, 881 | Close: func() error { return nil }, 882 | Check: func() bool { return true }, 883 | }, -1, SendTimeout(time.Second)) 884 | if err != nil { 885 | t.Fatalf("SpawnGeneric(_, %d , SendTimeout(%v)) failed: %v", -1, time.Second, err) 886 | } 887 | 888 | if err := wIn.Close(); err != nil { 889 | t.Fatalf("wIn.Close() failed: %v", err) 890 | } 891 | 892 | if err := exp.Send("test" + "\n"); err != nil { 893 | t.Fatalf("Send(%q) command failed: %v", "test"+"\n", err) 894 | } 895 | 896 | if err := exp.Send("test" + "\n"); err == nil { 897 | t.Errorf("Send(%q) = %t want: %t, err: %v", "test"+"\n", (err != nil), true, err) 898 | } 899 | waitCh <- nil 900 | exp.Close() 901 | wOut.Close() 902 | 903 | <-r 904 | } 905 | 906 | // TestSpawnSSHPTY tests the SSHPTY spawner. 907 | func TestSpawnSSHPTY(t *testing.T) { 908 | tests := []struct { 909 | name string 910 | fail bool 911 | srv []Batcher 912 | clt []Batcher 913 | sshNil bool 914 | srvTerm *term.Termios 915 | cltTerm term.Termios 916 | }{{ 917 | name: "sshClient broken", 918 | fail: true, 919 | sshNil: true, 920 | }, { 921 | name: "Empty Termios", 922 | clt: []Batcher{ 923 | &BSnd{"Hello"}, 924 | &BExp{"World"}, 925 | }, 926 | srv: []Batcher{ 927 | &BExp{"Hello"}, 928 | &BSnd{"World"}, 929 | }, 930 | }, { 931 | name: "Termios mismatch", 932 | fail: true, 933 | srvTerm: &term.Termios{ 934 | Wz: term.Winsize{ 935 | WsCol: 120, 936 | WsRow: 40, 937 | }}, 938 | cltTerm: term.Termios{ 939 | Wz: term.Winsize{ 940 | WsCol: 240, 941 | WsRow: 22, 942 | }, 943 | }, 944 | }} 945 | 946 | srv := NewSSHServer("test", "test", nil, nil) 947 | port, err := srv.Serve() 948 | if err != nil { 949 | t.Fatalf("srv.Serve failed: %v", err) 950 | } 951 | defer func() { 952 | if err := srv.Close(); err != nil { 953 | t.Errorf("srv.Close failed: %v", err) 954 | } 955 | }() 956 | 957 | for _, tst := range tests { 958 | srv.Batcher(tst.srv) 959 | srv.Termios(tst.srvTerm) 960 | var sshClt *ssh.Client 961 | if !tst.sshNil { 962 | clt, err := ssh.Dial("tcp", net.JoinHostPort("localhost", strconv.Itoa(int(port))), 963 | &ssh.ClientConfig{ 964 | User: "test", 965 | Auth: []ssh.AuthMethod{ssh.Password("test")}, 966 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 967 | }) 968 | if err != nil { 969 | t.Errorf("%s: net.Dial(%q) failed: %v", tst.name, net.JoinHostPort("localhost", strconv.Itoa(int(port))), err) 970 | continue 971 | } 972 | sshClt = clt 973 | } 974 | e, _, err := SpawnSSHPTY(sshClt, testTimeout*2, tst.cltTerm) 975 | if got, want := err != nil, tst.fail; got != want { 976 | t.Errorf("%s: SpawnSSH = %t want: %t, err: %v", tst.name, got, want, err) 977 | continue 978 | } 979 | if err != nil { 980 | continue 981 | } 982 | res, err := e.ExpectBatch(tst.clt, testTimeout*2) 983 | if err != nil { 984 | t.Errorf("%s: e.ExpectBatch failed: %v, out: %v", tst.name, err, res) 985 | continue 986 | } 987 | if err := sshClt.Close(); err != nil { 988 | t.Errorf("%s: clt.Close failed: %v", tst.name, err) 989 | } 990 | } 991 | } 992 | 993 | // TestOptions tests manipulating options. 994 | func TestOptions(t *testing.T) { 995 | tests := []struct { 996 | name string 997 | check func() bool 998 | opts []Option 999 | re *regexp.Regexp 1000 | fail bool 1001 | }{{ 1002 | name: "No options", 1003 | check: func() bool { return true }, 1004 | }, { 1005 | name: "No check option", 1006 | opts: []Option{NoCheck()}, 1007 | check: func() bool { return false }, 1008 | }, 1009 | } 1010 | 1011 | for _, tst := range tests { 1012 | rIn, wIn := io.Pipe() 1013 | rOut, wOut := io.Pipe() 1014 | go fakeCli(cliMap, rIn, wOut) 1015 | waitCh := make(chan error) 1016 | exp, r, err := SpawnGeneric(&GenOptions{ 1017 | In: wIn, 1018 | Out: rOut, 1019 | Wait: func() error { return <-waitCh }, 1020 | Close: func() error { return wIn.Close() }, 1021 | Check: tst.check}, -1, tst.opts...) 1022 | if err != nil { 1023 | t.Errorf("%s: SpawnGeneric failed: %v", tst.name, err) 1024 | continue 1025 | } 1026 | if got, want := exp.Send("\n\n") != nil, tst.fail; got != want { 1027 | t.Errorf("%s: exp.Send(\"\\n\\n\") = %t want: %t", tst.name, got, want) 1028 | } 1029 | waitCh <- nil 1030 | exp.Close() 1031 | wOut.Close() 1032 | 1033 | <-r 1034 | } 1035 | } 1036 | 1037 | // TestSpawn tests out the Spawn function 1038 | func TestSpawn(t *testing.T) { 1039 | tests := []struct { 1040 | name string 1041 | fail bool 1042 | cmd string 1043 | cmdErr bool 1044 | }{{ 1045 | name: "Spawn non executable fail", 1046 | fail: true, 1047 | cmd: "/etc/hosts", 1048 | }, { 1049 | name: "Nil return code", 1050 | cmd: "/bin/true", 1051 | }, { 1052 | name: "Non nil return code", 1053 | cmd: "/bin/false", 1054 | cmdErr: true, 1055 | }, { 1056 | name: "Spawn cat", 1057 | cmd: "/bin/cat", 1058 | cmdErr: true, 1059 | }} 1060 | 1061 | for _, tst := range tests { 1062 | e, errCh, err := Spawn(tst.cmd, 8*time.Second) 1063 | if got, want := err != nil, tst.fail; got != want { 1064 | t.Errorf("%s: Spawn(%q) = %t want: %t, err: %v", tst.name, tst.cmd, got, want, err) 1065 | continue 1066 | } 1067 | if err != nil { 1068 | continue 1069 | } 1070 | <-time.After(2 * time.Second) 1071 | if err := e.Close(); err != nil { 1072 | t.Logf("e.Close failed: %v", err) 1073 | } 1074 | res := <-errCh 1075 | if got, want := res != nil, tst.cmdErr; got != want { 1076 | t.Errorf("%s: errCh = %t want: %t, err: %v", tst.name, got, want, err) 1077 | continue 1078 | } 1079 | } 1080 | } 1081 | 1082 | // TestSpawnWithArgs tests that arguments with embedded spaces works. 1083 | func TestSpawnWithArgs(t *testing.T) { 1084 | args := []string{"echo", "a b"} 1085 | e, _, err := SpawnWithArgs(args, 400*time.Millisecond) 1086 | if err != nil { 1087 | t.Errorf("Spawn(echo 'a b') failed: %v", err) 1088 | } 1089 | 1090 | // Expected to match 1091 | _, _, err = e.Expect(regexp.MustCompile("a b"), 400*time.Millisecond) 1092 | if err != nil { 1093 | t.Errorf("Expect(a b) failed: %v", err) 1094 | } 1095 | 1096 | // Expected to not match 1097 | _, _, err = e.Expect(regexp.MustCompile("a b"), 400*time.Millisecond) 1098 | if err == nil { 1099 | t.Error("Expect(a b) to not match") 1100 | } 1101 | 1102 | e.Close() 1103 | } 1104 | 1105 | // TestExpect tests the Expect function. 1106 | func TestExpect(t *testing.T) { 1107 | tests := []struct { 1108 | name string 1109 | fail bool 1110 | srv []Batcher 1111 | timeout time.Duration 1112 | re *regexp.Regexp 1113 | re2 *regexp.Regexp 1114 | }{{ 1115 | name: "Match prompt", 1116 | srv: []Batcher{ 1117 | &BSnd{` 1118 | Pretty please don't hack my chassis 1119 | 1120 | router1> `}, 1121 | }, 1122 | re: regexp.MustCompile("hack"), 1123 | re2: regexp.MustCompile("router1>"), 1124 | timeout: 2 * time.Second, 1125 | }, { 1126 | name: "Match fail", 1127 | fail: true, 1128 | re: regexp.MustCompile("router1>"), 1129 | srv: []Batcher{ 1130 | &BSnd{` 1131 | Welcome 1132 | 1133 | Router42>`}, 1134 | }, 1135 | timeout: 1 * time.Second, 1136 | }} 1137 | 1138 | for _, tst := range tests { 1139 | exp, _, err := SpawnFake(tst.srv, tst.timeout, PartialMatch(true)) 1140 | if err != nil { 1141 | if !tst.fail { 1142 | t.Errorf("%s: SpawnFake failed: %v", tst.name, err) 1143 | } 1144 | continue 1145 | } 1146 | out, _, err := exp.Expect(tst.re, tst.timeout) 1147 | if got, want := err != nil, tst.fail; got != want { 1148 | t.Errorf("%s: Expect(%q,%v) = %t want: %t , err: %v, out: %q", tst.name, tst.re.String(), tst.timeout, got, want, err, out) 1149 | continue 1150 | } 1151 | out, _, err = exp.Expect(tst.re2, tst.timeout) 1152 | if got, want := err != nil, tst.fail; got != want { 1153 | t.Errorf("%s: Expect(%q,%v) = %t want: %t , err: %v, out: %q", tst.name, tst.re.String(), tst.timeout, got, want, err, out) 1154 | continue 1155 | } 1156 | } 1157 | } 1158 | 1159 | // TestScenarios reads and executes the expect/*.sh test scenarios. 1160 | func TestScenarios(t *testing.T) { 1161 | //path := runfiles.Path(expTestData) 1162 | files, err := filepath.Glob(expTestData + "/*.sh") 1163 | if err != nil || len(files) == 0 { 1164 | t.Fatalf("filepath.Glob(%q) failed: %v, not testfile found", expTestData+"/*.sh", err) 1165 | } 1166 | L1: 1167 | for _, f := range files { 1168 | _, file := filepath.Split(f) 1169 | tst, err := buildTest(file) 1170 | if err != nil { 1171 | t.Errorf("%s: buildTest(%q) failed: %v", file, file, err) 1172 | continue 1173 | } 1174 | // Spawn the testfile 1175 | exp, r, err := Spawn(f, 0) 1176 | if err != nil { 1177 | t.Errorf("%s: Spawn(%q,0) failed: %v", file, file, err) 1178 | continue 1179 | } 1180 | t.Log("Testing scenariofile:", file) 1181 | for _, ts := range tst { 1182 | switch ts.Cmd() { 1183 | case BatchExpect: 1184 | re := regexp.MustCompile(ts.Arg()) 1185 | to := ts.Timeout() 1186 | if to == 0 { 1187 | to = 30 * time.Second 1188 | } 1189 | o, _, err := exp.Expect(re, to) 1190 | if err != nil { 1191 | t.Errorf("%s: Expect(%q,%v) failed: %v, out: %q", file, ts.Arg(), to, err, o) 1192 | continue L1 1193 | } 1194 | t.Log("Scenario:", file, "expect:", ts.Arg(), " found") 1195 | if !re.MatchString(o) { 1196 | t.Fatalf("%s: Doublecheck failed re: %q output: %q", file, ts.Arg(), o) 1197 | continue L1 1198 | } 1199 | case BatchSend: 1200 | if err := exp.Send(ts.Arg()); err != nil { 1201 | t.Fatalf("%s: Send(%q) failed: %v", file, ts.Arg(), err) 1202 | continue L1 1203 | } 1204 | case BatchSwitchCase: 1205 | to := ts.Timeout() 1206 | if to == 0 { 1207 | to = 30 * time.Second 1208 | } 1209 | o, _, _, err := exp.ExpectSwitchCase(ts.Cases(), to) 1210 | if err != nil { 1211 | if err.Error() == "process not running" { 1212 | t.Logf("%s: exp.ExpectSwitchCase(%v,%v) failed: %v, process returned: %v", file, ts.Cases(), to, err, <-r) 1213 | } 1214 | t.Errorf("%s: ExpectSwitchCase failed: %v case: %v output: %q", file, err, ts.Cases(), o) 1215 | continue L1 1216 | } 1217 | } 1218 | } 1219 | if err := exp.Close(); err != nil { 1220 | t.Logf("exp.Close failed: %v", err) 1221 | 1222 | } 1223 | } 1224 | } 1225 | 1226 | // TestBatchScenarios runs through the scenarios again , this time as Batchjobs. 1227 | func TestBatchScenarios(t *testing.T) { 1228 | files, err := filepath.Glob(expTestData + "/*.sh") 1229 | if err != nil || len(files) == 0 { 1230 | t.Fatalf("filepath.Glob(%q) failed: %v, not testfile found", expTestData+"/*.sh", err) 1231 | } 1232 | for _, f := range files { 1233 | _, file := filepath.Split(f) 1234 | tsts, err := buildTest(file) 1235 | if err != nil { 1236 | t.Errorf("%s: buildTest(%q) failed: %v", f, f, err) 1237 | continue 1238 | } 1239 | batch := []Batcher{} 1240 | for _, tst := range tsts { 1241 | switch tst.Cmd() { 1242 | case BatchExpect: 1243 | batch = append(batch, &BExp{tst.Arg()}) 1244 | case BatchSend: 1245 | batch = append(batch, &BSnd{tst.Arg()}) 1246 | case BatchSwitchCase: 1247 | batch = append(batch, &BCas{tst.Cases()}) 1248 | } 1249 | } 1250 | exp, r, err := Spawn(f, 30*time.Second, Verbose(true)) 1251 | if err != nil { 1252 | t.Errorf("%s: Spawn(%q) failed: %v", file, file, err) 1253 | continue 1254 | } 1255 | res, err := exp.ExpectBatch(batch, 30*time.Second) 1256 | if err != nil { 1257 | t.Errorf("%s: ExpectBatch failed: %v, res: %v", file, err, res) 1258 | continue 1259 | } 1260 | exp.Close() 1261 | <-r 1262 | } 1263 | } 1264 | 1265 | var signalsInstalled = regexp.MustCompile("Waiting for signal") 1266 | 1267 | func TestSendSignal(t *testing.T) { 1268 | tests := []struct { 1269 | name string 1270 | fail bool 1271 | cmd string 1272 | sig os.Signal 1273 | expect string 1274 | }{{ 1275 | name: "Sig USR1", 1276 | cmd: "testdata/traptest.sh", 1277 | sig: syscall.SIGUSR1, 1278 | expect: "USR1", 1279 | }, { 1280 | name: "Sig HUP", 1281 | cmd: "testdata/traptest.sh", 1282 | sig: syscall.SIGHUP, 1283 | expect: "HUP", 1284 | }} 1285 | 1286 | for _, tst := range tests { 1287 | t.Run(tst.name, func(t *testing.T) { 1288 | exp, r, err := Spawn(tst.cmd, 30*time.Second, Verbose(true)) 1289 | if got, want := (err != nil), tst.fail; got != want { 1290 | t.Fatalf("%s: Spawn(%q, %v, _) = %t want: %t , err: %v", tst.name, tst.cmd, 30*time.Second, got, want, err) 1291 | } 1292 | defer func() { 1293 | exp.Close() 1294 | <-r 1295 | }() 1296 | if match, buf, err := exp.Expect(signalsInstalled, time.Second*10); err != nil { 1297 | t.Fatalf("%s: exp.Expect(%q, %v) failed: %v, match: %s, buf: %s", tst.name, tst.expect, time.Second*10, err, match, buf) 1298 | } 1299 | if err := exp.SendSignal(tst.sig); err != nil { 1300 | t.Fatalf("%s: exp.SendSignal(%v) failed: %v", tst.name, tst.sig, err) 1301 | } 1302 | 1303 | reExpect, err := regexp.Compile(tst.expect) 1304 | if err != nil { 1305 | t.Fatalf("%s: regexp.Compile(%q) failed: %v", tst.name, tst.expect, err) 1306 | } 1307 | 1308 | if match, buf, err := exp.Expect(reExpect, time.Second*2); err != nil { 1309 | t.Fatalf("%s: exp.Expect(%q, %v) failed: %v, match: %s, buf: %s", tst.name, tst.expect, time.Second*2, err, match, buf) 1310 | } 1311 | }) 1312 | } 1313 | } 1314 | 1315 | // ExampleGExpect_SendSignal shows the usage of the SendSignal call. 1316 | func ExampleGExpect_SendSignal() { 1317 | exp, r, err := Spawn("testdata/traptest.sh", 30*time.Second) 1318 | if err != nil { 1319 | fmt.Printf("Spawn failed: %v\n", err) 1320 | return 1321 | } 1322 | if match, buf, err := exp.Expect(signalsInstalled, time.Second*20); err != nil { 1323 | fmt.Printf("exp.Expect failed, match: %v, buf: %v, err: %v", match, buf, err) 1324 | return 1325 | } 1326 | 1327 | if err := exp.SendSignal(syscall.SIGUSR1); err != nil { 1328 | fmt.Printf("exp.SendSignal failed: %v", err) 1329 | return 1330 | } 1331 | 1332 | reExpect, err := regexp.Compile("USR1") 1333 | if err != nil { 1334 | fmt.Printf("regexp.Compile(%q) failed: %v", "Sig USR1", err) 1335 | return 1336 | } 1337 | 1338 | match, buf, err := exp.Expect(reExpect, time.Second*20) 1339 | if err != nil { 1340 | fmt.Printf("exp.Expect failed, match: %v, buf: %v, err: %v", match, buf, err) 1341 | return 1342 | } 1343 | fmt.Println(match) 1344 | <-r 1345 | // Output: Got the USR1 Signal 1346 | } 1347 | 1348 | // ExampleGExpect_SendSignal_Batch is an example of using SendSignal in a batch. 1349 | func ExampleGExpect_SendSignal_Batch() { 1350 | exp, r, err := Spawn("testdata/traptest.sh", 30*time.Second) 1351 | if err != nil { 1352 | fmt.Printf("Spawn failed: %v\n", err) 1353 | return 1354 | } 1355 | if _, err := exp.ExpectBatch([]Batcher{ 1356 | &BExp{`Waiting for signal`}, 1357 | &BSig{syscall.SIGUSR1}, 1358 | &BExp{`USR1`}, 1359 | }, time.Second*20); err != nil { 1360 | fmt.Printf("ExpectBatch failed: %v\n", err) 1361 | return 1362 | } 1363 | fmt.Println("Signal received") 1364 | <-r 1365 | // Output: Signal received 1366 | } 1367 | 1368 | var tMap map[string][]Batcher 1369 | 1370 | // buildTest Reads the sends and expected outputs from the testfiles eg. 1371 | func buildTest(fstring string) ([]Batcher, error) { 1372 | if tMap == nil { 1373 | tMap = make(map[string][]Batcher) 1374 | } 1375 | if tst, ok := tMap[fstring]; ok { 1376 | return tst, nil 1377 | } 1378 | f, err := os.Open(expTestData + "/" + fstring) 1379 | if err != nil { 1380 | return []Batcher{}, err 1381 | } 1382 | defer f.Close() 1383 | scn := bufio.NewScanner(f) 1384 | var ( 1385 | etst []Batcher 1386 | // tcases temporary []Caser slice 1387 | tcases []Caser 1388 | // incases toggle to tell if we're currently building a []Caser slice for BatchSwitchCase 1389 | incases bool 1390 | ) 1391 | for scn.Scan() { 1392 | ln := scn.Text() 1393 | if err := scn.Err(); err != nil { 1394 | return []Batcher{}, err 1395 | } 1396 | if res := cReg.FindStringSubmatch(ln); res != nil { 1397 | incases = true 1398 | res[2] = strings.Replace(res[2], `\n`, "\n", -1) 1399 | tcases = append(tcases, &Case{regexp.MustCompile(res[1]), res[2], nil, 0}) 1400 | continue 1401 | } 1402 | if res := eReg.FindStringSubmatch(ln); res != nil { 1403 | if incases { 1404 | incases = false 1405 | etst = append(etst, &BCas{tcases}) 1406 | tcases = []Caser{} 1407 | } 1408 | res[1] = strings.Replace(res[1], `\n`, "\n", -1) 1409 | etst = append(etst, &BExp{res[1]}) 1410 | continue 1411 | } 1412 | if res := sReg.FindStringSubmatch(ln); res != nil { 1413 | if incases { 1414 | incases = false 1415 | etst = append(etst, &BCas{tcases}) 1416 | tcases = []Caser{} 1417 | } 1418 | res[1] = strings.Replace(res[1], `\n`, "\n", -1) 1419 | etst = append(etst, &BSnd{res[1]}) 1420 | continue 1421 | } 1422 | } 1423 | // If c: is the last thing we have we need to tie it up 1424 | if incases { 1425 | etst = append(etst, &BCas{tcases}) 1426 | } 1427 | tMap[fstring] = etst 1428 | return etst, nil 1429 | } 1430 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/goexpect 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b 7 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f 8 | github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b 9 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de 10 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 // indirect 11 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 // indirect 12 | google.golang.org/grpc v1.31.0 13 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 6 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 7 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 8 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 9 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 10 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 11 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 15 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 16 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 17 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= 19 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 20 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 21 | github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b h1:VfPXB/wCGGt590QhD1bOpv2J/AmC/RJNTg/Q59HKSB0= 22 | github.com/ziutek/telnet v0.0.0-20180329124119-c3b780dc415b/go.mod h1:IZpXDfkJ6tWD3PhBK5YzgQT+xJWh7OsdwiG8hA2MkO4= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 25 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 26 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 27 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 28 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 29 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 30 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 31 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 32 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 33 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 36 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 37 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 38 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 39 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 40 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 41 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 44 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 45 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 46 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 47 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 49 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 50 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 51 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 52 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 53 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 54 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 55 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 56 | google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= 57 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 58 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 59 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 60 | -------------------------------------------------------------------------------- /testdata/menuloop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while ((1)) 3 | do 4 | SM=( "SLAVE" "SLAVE" "SLAVE") 5 | idx=$((RANDOM % 3)) 6 | SM[$idx]="MASTER" 7 | cat <<~~ 8 | -=# SuperCLI #=- 9 | Please choose one controller from the list: 10 | 0 -- Controller1 (${SM[0]}) 11 | 1 -- Controller2 (${SM[1]}) 12 | 2 -- Controller3 (${SM[2]}) 13 | Please enter a choice (by default, connect to master): 14 | ~~ 15 | # c: '0 -- .*\(MASTER\)' 0\n 16 | # c: '1 -- .*\(MASTER\)' 1\n 17 | # c: '2 -- .*\(MASTER\)' 2\n 18 | read i 19 | if [[ $i != $idx ]] 20 | then 21 | exit 1 22 | fi 23 | # e: Controller[0-2]/> 24 | # s: \n 25 | echo -n "Controller$idx/> " 26 | read 27 | done 28 | -------------------------------------------------------------------------------- /testdata/traptest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | FIFOFILE=$(mktemp -u) 3 | trap 'rm -f $FIFOFILE; echo "Got the INTR Signal"; exit' INT 4 | trap 'rm -f $FIFOFILE; echo "Got the QUIT Signal"; exit' QUIT 5 | trap 'rm -f $FIFOFILE; echo "Got the USR1 Signal"; exit' USR1 6 | trap 'rm -f $FIFOFILE; echo "Got the HUP Signal"; exit' HUP 7 | 8 | if [[ -p $FIFOFILE ]] 9 | then 10 | rm -f $FIFOFILE 11 | fi 12 | 13 | mkfifo -m 0400 $FIFOFILE 14 | echo "Waiting for signal" 15 | true < $FIFOFILE 16 | sleep 10 17 | --------------------------------------------------------------------------------