lib/expectr.rb in expectr-1.1.0 vs lib/expectr.rb in expectr-1.1.1
- old
+ new
@@ -28,13 +28,19 @@
# # Do stuff if we see 'WEB' in the output
# else
# # Do other stuff
# end
class Expectr
+ DEFAULT_TIMEOUT = 30
+ DEFAULT_FLUSH_BUFFER = true
+ DEFAULT_BUFFER_SIZE = 8192
+ DEFAULT_CONSTRAIN = false
+ DEFAULT_FORCE_MATCH = false
+
# Public: Gets/sets the number of seconds a call to Expectr#expect may last
attr_accessor :timeout
- # Public: Gets/sets whether to flush program output to STDOUT
+ # Public: Gets/sets whether to flush program output to $stdout
attr_accessor :flush_buffer
# Public: Gets/sets the number of bytes to use for the internal buffer
attr_accessor :buffer_size
# Public: Gets/sets whether to constrain the buffer to the buffer size
attr_accessor :constrain
@@ -67,56 +73,25 @@
# 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)
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)
+ raise ArgumentError, "String or File expected" unless cmd.kind_of?(String)
- 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
-
+ parse_options(args)
+ @buffer = ''
+ @discard = ''
@out_mutex = Mutex.new
@out_update = false
@interact = false
@stdout,@stdin,@pid = PTY.spawn(cmd)
- @stdout.winsize = STDOUT.winsize
+ @stdout.winsize = $stdout.winsize if $stdout.tty?
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
+ process_output
end
Thread.new do
Process.wait @pid
@pid = 0
@@ -137,45 +112,23 @@
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")
+ env = prepare_interact_environment
+ input = ''
+
while @pid > 0 && @interact
- if select([STDIN], nil, nil, 1)
- c = STDIN.getc.chr
+ 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
+ restore_environment(env)
end
blocking ? interact.join : interact
end
@@ -254,31 +207,19 @@
# 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
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
+ @out_update ||= @force_match
+ pattern = Regexp.new(Regexp.quote(pattern)) if pattern.kind_of?(String)
+ unless pattern.kind_of?(Regexp)
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
+ match = check_match(pattern)
end
@out_mutex.synchronize do
@discard = @buffer[0..match.begin(0)-1]
@buffer = @buffer[match.end(0)..-1]
@@ -294,11 +235,11 @@
# Public: Clear output buffer
#
# Returns nothing.
def clear_buffer!
@out_mutex.synchronize do
- @buffer = ''.encode("UTF-8")
+ @buffer = ''
@out_update = false
end
end
# Public: Return the child's window size.
@@ -306,25 +247,147 @@
# Returns a two-element array (same as IO#winsize).
def winsize
@stdout.winsize
end
- # Internal: Print buffer to STDOUT if @flush_buffer is true
+ # Internal: Print buffer to $stdout if @flush_buffer is true
#
- # buf - String to be printed to STDOUT
+ # buf - String to be printed to $stdout
#
# Returns nothing.
def print_buffer(buf)
print buf if @flush_buffer
- STDOUT.flush unless STDOUT.sync
+ $stdout.flush unless $stdout.sync
end
# 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.
def force_utf8(buf)
+ return buf if buf.valid_encoding?
buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)
end
+
+ private
+
+ # Internal: Determine values of instance options and set instance variables
+ # appropriately, allowing for default values where nothing is passed.
+ #
+ # 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.
+ # :flush_buffer - Whether to flush output of the process to the
+ # console.
+ # :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.
+ # :constrain - Whether to constrain the internal buffer from the
+ # sub-process to :buffer_size.
+ # :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.
+ #
+ # Returns nothing.
+ def parse_options(args)
+ @timeout = args[:timeout] || DEFAULT_TIMEOUT
+ @buffer_size = args[:buffer_size] || DEFAULT_BUFFER_SIZE
+ @constrain = args[:constrain] || DEFAULT_CONSTRAIN
+ @force_match = args[:force_match] || DEFAULT_FORCE_MATCH
+ @flush_buffer = args[:flush_buffer]
+ @flush_buffer = DEFAULT_FLUSH_BUFFER if @flush_buffer.nil?
+ end
+
+ # Internal: Read from the process's stdout. Force UTF-8 encoding, append to
+ # the internal buffer, and print to $stdout if appropriate.
+ #
+ # Returns nothing.
+ def process_output
+ while @pid > 0
+ unless select([@stdout], nil, nil, @timeout).nil?
+ buf = ''
+
+ begin
+ @stdout.sysread(@buffer_size, buf)
+ rescue Errno::EIO #Application went away.
+ @pid = 0
+ return
+ end
+
+ print_buffer(force_utf8(buf))
+
+ @out_mutex.synchronize do
+ @buffer << buf
+ if @constrain && @buffer.length > @buffer_size
+ @buffer = @buffer[-@buffer_size..-1]
+ end
+ @out_update = true
+ end
+ end
+ end
+ end
+
+ # Internal: Prepare environment for interact mode, saving original
+ # environment parameters.
+ #
+ # Returns a Hash object with two keys: :tty containing original tty
+ # information and :sig containing signal handlers which have been replaced.
+ def prepare_interact_environment
+ env = {sig: {}}
+ # Save our old tty settings and set up our new environment
+ env[:tty] = `stty -g`
+ `stty -icanon min 1 time 0 -echo`
+
+ # SIGINT should be sent along to the process.
+ env[:sig]['INT'] = trap 'INT' do
+ send "\C-c"
+ end
+
+ # SIGTSTP should be sent along to the process as well.
+ env[:sig]['TSTP'] = trap 'TSTP' do
+ send "\C-z"
+ end
+
+ # SIGWINCH should trigger an update to the child process
+ env[:sig]['WINCH'] = trap 'WINCH' do
+ @stdout.winsize = $stdout.winsize
+ end
+
+ @interact = true
+ env
+ end
+
+ # Internal: Restore environment post interact mode from saved parameters.
+ #
+ # Returns nothing.
+ def restore_environment(env)
+ env[:sig].each_key do |sig|
+ trap sig, env[:sig][sig]
+ end
+ `stty #{env[:tty]}`
+ @interact = false
+ end
+
+ # Internal: Check for a match against a given pattern until a match is found.
+ # This method should be wrapped in a Timeout block or otherwise have some
+ # mechanism to break out of the loop.
+ #
+ # Returns a MatchData object containing the match found.
+ def check_match(pattern)
+ match = nil
+ while match.nil?
+ if @out_update
+ @out_mutex.synchronize do
+ match = pattern.match(@buffer)
+ @out_update = false
+ end
+ end
+ sleep 0.1
+ end
+ match
+ end
end