Class: Expectr

Inherits:
Object
  • Object
show all
Defined in:
lib/expectr.rb,
lib/expectr/error.rb,
lib/expectr/version.rb

Overview

Public: Expectr is an API to the functionality of Expect (see expect.nist.gov) implemented in ruby.

Expectr contrasts with Ruby’s built-in Expect class by avoiding tying in with the IO class, instead creating a new object entirely to allow for more grainular control over the execution and display of the program being run.

Examples

# SSH Login to another machine
exp = Expectr.new('ssh [email protected]')
exp.expect("Password:")
exp.send('password')
exp.interact!(blocking: true)

# See if a web server is running on the local host, react accordingly
exp = Expectr.new('netstat -ntl|grep ":80 " && echo "WEB"', timeout: 1)
if exp.expeect("WEB")
  # Do stuff if we see 'WEB' in the output
else
  # Do other stuff
end

Defined Under Namespace

Classes: ProcessError

Constant Summary collapse

VERSION =
'1.1.0'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cmd, args = {}) ⇒ Expectr

Public: Initialize a new Expectr object. Spawns a sub-process and attaches to STDIN and STDOUT for the new process.

cmd - A String or File referencing the application to launch args - A Hash used to specify options for the new object (default: {}):

:timeout      - Number of seconds that a call to Expectr#expect has
                to complete (default: 30)
:flush_buffer - Whether to flush output of the process to the
                console (default: true)
:buffer_size  - Number of bytes to attempt to read from sub-process
                at a time.  If :constrain is true, this will be the
                maximum size of the internal buffer as well.
                (default: 8192)
:constrain    - Whether to constrain the internal buffer from the
                sub-process to :buffer_size (default: false)
:force_match  - Whether to always attempt to match against the
                internal buffer on a call to Expectr#expect.  This
                is relevant following a failed call to
                Expectr#expect, which will leave the update status
                set to false, preventing further matches until more
                output is generated otherwise. (default: false)


71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/expectr.rb', line 71

def initialize(cmd, args={})
  unless cmd.kind_of? String or cmd.kind_of? File
    raise ArgumentError, "String or File expected"
  end

  cmd = cmd.path if cmd.kind_of? File

  @buffer = ''.encode("UTF-8")
  @discard = ''.encode("UTF-8")

  @timeout = args[:timeout] || 30
  @flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer]
  @buffer_size = args[:buffer_size] || 8192
  @constrain = args[:constrain] || false
  @force_match = args[:force_match] || false

  @out_mutex = Mutex.new
  @out_update = false
  @interact = false

  @stdout,@stdin,@pid = PTY.spawn(cmd)
  @stdout.winsize = STDOUT.winsize

  Thread.new do
    while @pid > 0
      unless select([@stdout], nil, nil, @timeout).nil?
        buf = ''.encode("UTF-8")

        begin
          @stdout.sysread(@buffer_size, buf)
        rescue Errno::EIO #Application went away.
          @pid = 0
          break
        end

        force_utf8(buf) unless buf.valid_encoding?
        print_buffer(buf)

        @out_mutex.synchronize do
          @buffer << buf
          if @buffer.length > @buffer_size && @constrain
            @buffer = @buffer[-@buffer_size..-1]
          end
          @out_update = true
        end
      end
    end
  end

  Thread.new do
    Process.wait @pid
    @pid = 0
  end
end

Instance Attribute Details

#bufferObject (readonly)

Public: Returns the active buffer to match against



46
47
48
# File 'lib/expectr.rb', line 46

def buffer
  @buffer
end

#buffer_sizeObject

Public: Gets/sets the number of bytes to use for the internal buffer



38
39
40
# File 'lib/expectr.rb', line 38

def buffer_size
  @buffer_size
end

#constrainObject

Public: Gets/sets whether to constrain the buffer to the buffer size



40
41
42
# File 'lib/expectr.rb', line 40

def constrain
  @constrain
end

#discardObject (readonly)

Public: Returns the buffer discarded by the latest call to Expectr#expect



48
49
50
# File 'lib/expectr.rb', line 48

def discard
  @discard
end

#flush_bufferObject

Public: Gets/sets whether to flush program output to STDOUT



36
37
38
# File 'lib/expectr.rb', line 36

def flush_buffer
  @flush_buffer
end

#force_matchObject

Public: Whether to always attempt to match once on calls to Expectr#expect.



42
43
44
# File 'lib/expectr.rb', line 42

def force_match
  @force_match
end

#pidObject (readonly)

Public: Returns the PID of the running process



44
45
46
# File 'lib/expectr.rb', line 44

def pid
  @pid
end

#timeoutObject

Public: Gets/sets the number of seconds a call to Expectr#expect may last



34
35
36
# File 'lib/expectr.rb', line 34

def timeout
  @timeout
end

Instance Method Details

#clear_buffer!Object

Public: Clear output buffer

Returns nothing.



297
298
299
300
301
302
# File 'lib/expectr.rb', line 297

def clear_buffer!
  @out_mutex.synchronize do
    @buffer = ''.encode("UTF-8")
    @out_update = false
  end
end

#expect(pattern, recoverable = false) ⇒ Object

Public: Begin a countdown and search for a given String or Regexp in the output buffer.

pattern - String or Regexp representing what we want to find recoverable - Denotes whether failing to match the pattern should cause the

method to raise an exception (default: false)

Examples

exp.expect("this should exist")
# => MatchData

exp.expect("this should exist") do
  # ...
end

exp.expect(/not there/)
# Raises Timeout::Error

exp.expect(/not there/, true)
# => nil

Returns a MatchData object once a match is found if no block is given Yields the MatchData object representing the match Raises TypeError if something other than a String or Regexp is given Raises Timeout::Error if a match isn’t found in time, unless recoverable



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/expectr.rb', line 257

def expect(pattern, recoverable = false)
  match = nil
  @out_update = true if @force_match

  case pattern
  when String
    pattern = Regexp.new(Regexp.quote(pattern))
  when Regexp
  else
    raise TypeError, "Pattern class should be String or Regexp"
  end

  begin
    Timeout::timeout(@timeout) do
      while match.nil?
        if @out_update
          @out_mutex.synchronize do
            match = pattern.match @buffer
            @out_update = false
          end
        end
        sleep 0.1
      end
    end

    @out_mutex.synchronize do
      @discard = @buffer[0..match.begin(0)-1]
      @buffer = @buffer[match.end(0)..-1]
      @out_update = true
    end
  rescue Timeout::Error => details
    raise details unless recoverable
  end

  block_given? ? yield(match) : match
end

#force_utf8(buf) ⇒ Object

Internal: Encode a String twice to force UTF-8 encoding, dropping

problematic characters in the process.

buf - String to be encoded.

Returns the encoded String.



327
328
329
# File 'lib/expectr.rb', line 327

def force_utf8(buf)                                                           
  buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)     
end

#interact!(args = {}) ⇒ Object

Public: Relinquish control of the running process to the controlling terminal, acting as a pass-through for the life of the process. SIGINT will be caught and sent to the application as “C-c”.

args - A Hash used to specify options to be used for interaction (default:

{}):
:flush_buffer - explicitly set @flush_buffer to the value specified
:blocking     - Whether to block on this call or allow code
                execution to continue (default: false)

Returns the interaction Thread

Raises:



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/expectr.rb', line 137

def interact!(args = {})
  raise ProcessError if @interact

  blocking = args[:blocking] || false
  @flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer]
  @interact = true

  # Save our old tty settings and set up our new environment
  old_tty = `stty -g`
  `stty -icanon min 1 time 0 -echo`

  # SIGINT should be sent along to the process.
  old_int_trap = trap 'INT' do
    send "\C-c"
  end

  # SIGTSTP should be sent along to the process as well.
  old_tstp_trap = trap 'TSTP' do
    send "\C-z"
  end

  # SIGWINCH should trigger an update to the child process
  old_winch_trap = trap 'WINCH' do
    @stdout.winsize = STDOUT.winsize
  end
  
  interact = Thread.new do
    input = ''.encode("UTF-8")
    while @pid > 0 && @interact
      if select([STDIN], nil, nil, 1)
        c = STDIN.getc.chr
        send c unless c.nil?
      end
    end

    trap 'INT', old_int_trap
    trap 'TSTP', old_tstp_trap
    trap 'WINCH', old_winch_trap
    `stty #{old_tty}`
    @interact = false
  end

  blocking ? interact.join : interact
end

#interact?Boolean

Public: Report whether or not current Expectr object is in interact mode

Returns true or false

Returns:

  • (Boolean)


185
186
187
# File 'lib/expectr.rb', line 185

def interact?
  @interact
end

#kill!(signal = :TERM) ⇒ Object

Public: Kill the running process, raise ProcessError if the pid isn’t > 1

signal - Symbol, String, or Fixnum representing the signal to send to the

running process. (default: :TERM)

Returns true if the process was successfully killed, false otherwise

Raises:



202
203
204
205
# File 'lib/expectr.rb', line 202

def kill!(signal=:TERM)
  raise ProcessError unless @pid > 0
  (Process::kill(signal.to_sym, @pid) == 1)
end

#leave!Object

Public: Cause the current Expectr object to drop out of interact mode

Returns nothing.



192
193
194
# File 'lib/expectr.rb', line 192

def leave!
  @interact=false
end

Internal: Print buffer to STDOUT if @flush_buffer is true

buf - String to be printed to STDOUT

Returns nothing.



316
317
318
319
# File 'lib/expectr.rb', line 316

def print_buffer(buf)
  print buf if @flush_buffer
  STDOUT.flush unless STDOUT.sync
end

#puts(str = '') ⇒ Object

Public: Wraps Expectr#send, appending a newline to the end of the string

str - String to be sent to the active process (default: ”)

Returns nothing.



227
228
229
# File 'lib/expectr.rb', line 227

def puts(str = '')
  send str + "\n"
end

#send(str) ⇒ Object

Public: Send input to the active process

str - String to be sent to the active process

Returns nothing. Raises Expectr::ProcessError if the process isn’t running



213
214
215
216
217
218
219
220
# File 'lib/expectr.rb', line 213

def send(str)
  begin
    @stdin.syswrite str
  rescue Errno::EIO #Application went away.
    @pid = 0
  end
  raise Expectr::ProcessError unless @pid > 0
end

#winsizeObject

Public: Return the child’s window size.

Returns a two-element array (same as IO#winsize).



307
308
309
# File 'lib/expectr.rb', line 307

def winsize
  @stdout.winsize
end