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.0.3'

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)


70
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
# File 'lib/expectr.rb', line 70

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)

  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



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

def buffer
  @buffer
end

#buffer_sizeObject

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



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

def buffer_size
  @buffer_size
end

#constrainObject

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



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

def constrain
  @constrain
end

#discardObject (readonly)

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



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

def discard
  @discard
end

#flush_bufferObject

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



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

def flush_buffer
  @flush_buffer
end

#force_matchObject

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



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

def force_match
  @force_match
end

#pidObject (readonly)

Public: Returns the PID of the running process



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

def pid
  @pid
end

#timeoutObject

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



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

def timeout
  @timeout
end

Instance Method Details

#clear_buffer!Object

Public: Clear output buffer

Returns nothing.



289
290
291
292
293
294
# File 'lib/expectr.rb', line 289

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



249
250
251
252
253
254
255
256
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
# File 'lib/expectr.rb', line 249

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.



312
313
314
# File 'lib/expectr.rb', line 312

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:



135
136
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
# File 'lib/expectr.rb', line 135

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
  
  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
    `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)


177
178
179
# File 'lib/expectr.rb', line 177

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:



194
195
196
197
# File 'lib/expectr.rb', line 194

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.



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

def leave!
  @interact=false
end

Internal: Print buffer to STDOUT if @flush_buffer is true

buf - String to be printed to STDOUT

Returns nothing.



301
302
303
304
# File 'lib/expectr.rb', line 301

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.



219
220
221
# File 'lib/expectr.rb', line 219

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



205
206
207
208
209
210
211
212
# File 'lib/expectr.rb', line 205

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