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