| import time |
| |
| from .exceptions import EOF, TIMEOUT |
| |
| class Expecter(object): |
| def __init__(self, spawn, searcher, searchwindowsize=-1): |
| self.spawn = spawn |
| self.searcher = searcher |
| # A value of -1 means to use the figure from spawn, which should |
| # be None or a positive number. |
| if searchwindowsize == -1: |
| searchwindowsize = spawn.searchwindowsize |
| self.searchwindowsize = searchwindowsize |
| self.lookback = None |
| if hasattr(searcher, 'longest_string'): |
| self.lookback = searcher.longest_string |
| |
| def do_search(self, window, freshlen): |
| spawn = self.spawn |
| searcher = self.searcher |
| if freshlen > len(window): |
| freshlen = len(window) |
| index = searcher.search(window, freshlen, self.searchwindowsize) |
| if index >= 0: |
| spawn._buffer = spawn.buffer_type() |
| spawn._buffer.write(window[searcher.end:]) |
| spawn.before = spawn._before.getvalue()[ |
| 0:-(len(window) - searcher.start)] |
| spawn._before = spawn.buffer_type() |
| spawn._before.write(window[searcher.end:]) |
| spawn.after = window[searcher.start:searcher.end] |
| spawn.match = searcher.match |
| spawn.match_index = index |
| # Found a match |
| return index |
| elif self.searchwindowsize or self.lookback: |
| maintain = self.searchwindowsize or self.lookback |
| if spawn._buffer.tell() > maintain: |
| spawn._buffer = spawn.buffer_type() |
| spawn._buffer.write(window[-maintain:]) |
| |
| def existing_data(self): |
| # First call from a new call to expect_loop or expect_async. |
| # self.searchwindowsize may have changed. |
| # Treat all data as fresh. |
| spawn = self.spawn |
| before_len = spawn._before.tell() |
| buf_len = spawn._buffer.tell() |
| freshlen = before_len |
| if before_len > buf_len: |
| if not self.searchwindowsize: |
| spawn._buffer = spawn.buffer_type() |
| window = spawn._before.getvalue() |
| spawn._buffer.write(window) |
| elif buf_len < self.searchwindowsize: |
| spawn._buffer = spawn.buffer_type() |
| spawn._before.seek( |
| max(0, before_len - self.searchwindowsize)) |
| window = spawn._before.read() |
| spawn._buffer.write(window) |
| else: |
| spawn._buffer.seek(max(0, buf_len - self.searchwindowsize)) |
| window = spawn._buffer.read() |
| else: |
| if self.searchwindowsize: |
| spawn._buffer.seek(max(0, buf_len - self.searchwindowsize)) |
| window = spawn._buffer.read() |
| else: |
| window = spawn._buffer.getvalue() |
| return self.do_search(window, freshlen) |
| |
| def new_data(self, data): |
| # A subsequent call, after a call to existing_data. |
| spawn = self.spawn |
| freshlen = len(data) |
| spawn._before.write(data) |
| if not self.searchwindowsize: |
| if self.lookback: |
| # search lookback + new data. |
| old_len = spawn._buffer.tell() |
| spawn._buffer.write(data) |
| spawn._buffer.seek(max(0, old_len - self.lookback)) |
| window = spawn._buffer.read() |
| else: |
| # copy the whole buffer (really slow for large datasets). |
| spawn._buffer.write(data) |
| window = spawn.buffer |
| else: |
| if len(data) >= self.searchwindowsize or not spawn._buffer.tell(): |
| window = data[-self.searchwindowsize:] |
| spawn._buffer = spawn.buffer_type() |
| spawn._buffer.write(window[-self.searchwindowsize:]) |
| else: |
| spawn._buffer.write(data) |
| new_len = spawn._buffer.tell() |
| spawn._buffer.seek(max(0, new_len - self.searchwindowsize)) |
| window = spawn._buffer.read() |
| return self.do_search(window, freshlen) |
| |
| def eof(self, err=None): |
| spawn = self.spawn |
| |
| spawn.before = spawn._before.getvalue() |
| spawn._buffer = spawn.buffer_type() |
| spawn._before = spawn.buffer_type() |
| spawn.after = EOF |
| index = self.searcher.eof_index |
| if index >= 0: |
| spawn.match = EOF |
| spawn.match_index = index |
| return index |
| else: |
| spawn.match = None |
| spawn.match_index = None |
| msg = str(spawn) |
| msg += '\nsearcher: %s' % self.searcher |
| if err is not None: |
| msg = str(err) + '\n' + msg |
| |
| exc = EOF(msg) |
| exc.__cause__ = None # in Python 3.x we can use "raise exc from None" |
| raise exc |
| |
| def timeout(self, err=None): |
| spawn = self.spawn |
| |
| spawn.before = spawn._before.getvalue() |
| spawn.after = TIMEOUT |
| index = self.searcher.timeout_index |
| if index >= 0: |
| spawn.match = TIMEOUT |
| spawn.match_index = index |
| return index |
| else: |
| spawn.match = None |
| spawn.match_index = None |
| msg = str(spawn) |
| msg += '\nsearcher: %s' % self.searcher |
| if err is not None: |
| msg = str(err) + '\n' + msg |
| |
| exc = TIMEOUT(msg) |
| exc.__cause__ = None # in Python 3.x we can use "raise exc from None" |
| raise exc |
| |
| def errored(self): |
| spawn = self.spawn |
| spawn.before = spawn._before.getvalue() |
| spawn.after = None |
| spawn.match = None |
| spawn.match_index = None |
| |
| def expect_loop(self, timeout=-1): |
| """Blocking expect""" |
| spawn = self.spawn |
| |
| if timeout is not None: |
| end_time = time.time() + timeout |
| |
| try: |
| idx = self.existing_data() |
| if idx is not None: |
| return idx |
| while True: |
| # No match at this point |
| if (timeout is not None) and (timeout < 0): |
| return self.timeout() |
| # Still have time left, so read more data |
| incoming = spawn.read_nonblocking(spawn.maxread, timeout) |
| if self.spawn.delayafterread is not None: |
| time.sleep(self.spawn.delayafterread) |
| idx = self.new_data(incoming) |
| # Keep reading until exception or return. |
| if idx is not None: |
| return idx |
| if timeout is not None: |
| timeout = end_time - time.time() |
| except EOF as e: |
| return self.eof(e) |
| except TIMEOUT as e: |
| return self.timeout(e) |
| except: |
| self.errored() |
| raise |
| |
| |
| class searcher_string(object): |
| '''This is a plain string search helper for the spawn.expect_any() method. |
| This helper class is for speed. For more powerful regex patterns |
| see the helper class, searcher_re. |
| |
| Attributes: |
| |
| eof_index - index of EOF, or -1 |
| timeout_index - index of TIMEOUT, or -1 |
| |
| After a successful match by the search() method the following attributes |
| are available: |
| |
| start - index into the buffer, first byte of match |
| end - index into the buffer, first byte after match |
| match - the matching string itself |
| |
| ''' |
| |
| def __init__(self, strings): |
| '''This creates an instance of searcher_string. This argument 'strings' |
| may be a list; a sequence of strings; or the EOF or TIMEOUT types. ''' |
| |
| self.eof_index = -1 |
| self.timeout_index = -1 |
| self._strings = [] |
| self.longest_string = 0 |
| for n, s in enumerate(strings): |
| if s is EOF: |
| self.eof_index = n |
| continue |
| if s is TIMEOUT: |
| self.timeout_index = n |
| continue |
| self._strings.append((n, s)) |
| if len(s) > self.longest_string: |
| self.longest_string = len(s) |
| |
| def __str__(self): |
| '''This returns a human-readable string that represents the state of |
| the object.''' |
| |
| ss = [(ns[0], ' %d: %r' % ns) for ns in self._strings] |
| ss.append((-1, 'searcher_string:')) |
| if self.eof_index >= 0: |
| ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) |
| if self.timeout_index >= 0: |
| ss.append((self.timeout_index, |
| ' %d: TIMEOUT' % self.timeout_index)) |
| ss.sort() |
| ss = list(zip(*ss))[1] |
| return '\n'.join(ss) |
| |
| def search(self, buffer, freshlen, searchwindowsize=None): |
| '''This searches 'buffer' for the first occurrence of one of the search |
| strings. 'freshlen' must indicate the number of bytes at the end of |
| 'buffer' which have not been searched before. It helps to avoid |
| searching the same, possibly big, buffer over and over again. |
| |
| See class spawn for the 'searchwindowsize' argument. |
| |
| If there is a match this returns the index of that string, and sets |
| 'start', 'end' and 'match'. Otherwise, this returns -1. ''' |
| |
| first_match = None |
| |
| # 'freshlen' helps a lot here. Further optimizations could |
| # possibly include: |
| # |
| # using something like the Boyer-Moore Fast String Searching |
| # Algorithm; pre-compiling the search through a list of |
| # strings into something that can scan the input once to |
| # search for all N strings; realize that if we search for |
| # ['bar', 'baz'] and the input is '...foo' we need not bother |
| # rescanning until we've read three more bytes. |
| # |
| # Sadly, I don't know enough about this interesting topic. /grahn |
| |
| for index, s in self._strings: |
| if searchwindowsize is None: |
| # the match, if any, can only be in the fresh data, |
| # or at the very end of the old data |
| offset = -(freshlen + len(s)) |
| else: |
| # better obey searchwindowsize |
| offset = -searchwindowsize |
| n = buffer.find(s, offset) |
| if n >= 0 and (first_match is None or n < first_match): |
| first_match = n |
| best_index, best_match = index, s |
| if first_match is None: |
| return -1 |
| self.match = best_match |
| self.start = first_match |
| self.end = self.start + len(self.match) |
| return best_index |
| |
| |
| class searcher_re(object): |
| '''This is regular expression string search helper for the |
| spawn.expect_any() method. This helper class is for powerful |
| pattern matching. For speed, see the helper class, searcher_string. |
| |
| Attributes: |
| |
| eof_index - index of EOF, or -1 |
| timeout_index - index of TIMEOUT, or -1 |
| |
| After a successful match by the search() method the following attributes |
| are available: |
| |
| start - index into the buffer, first byte of match |
| end - index into the buffer, first byte after match |
| match - the re.match object returned by a successful re.search |
| |
| ''' |
| |
| def __init__(self, patterns): |
| '''This creates an instance that searches for 'patterns' Where |
| 'patterns' may be a list or other sequence of compiled regular |
| expressions, or the EOF or TIMEOUT types.''' |
| |
| self.eof_index = -1 |
| self.timeout_index = -1 |
| self._searches = [] |
| for n, s in enumerate(patterns): |
| if s is EOF: |
| self.eof_index = n |
| continue |
| if s is TIMEOUT: |
| self.timeout_index = n |
| continue |
| self._searches.append((n, s)) |
| |
| def __str__(self): |
| '''This returns a human-readable string that represents the state of |
| the object.''' |
| |
| #ss = [(n, ' %d: re.compile("%s")' % |
| # (n, repr(s.pattern))) for n, s in self._searches] |
| ss = list() |
| for n, s in self._searches: |
| ss.append((n, ' %d: re.compile(%r)' % (n, s.pattern))) |
| ss.append((-1, 'searcher_re:')) |
| if self.eof_index >= 0: |
| ss.append((self.eof_index, ' %d: EOF' % self.eof_index)) |
| if self.timeout_index >= 0: |
| ss.append((self.timeout_index, ' %d: TIMEOUT' % |
| self.timeout_index)) |
| ss.sort() |
| ss = list(zip(*ss))[1] |
| return '\n'.join(ss) |
| |
| def search(self, buffer, freshlen, searchwindowsize=None): |
| '''This searches 'buffer' for the first occurrence of one of the regular |
| expressions. 'freshlen' must indicate the number of bytes at the end of |
| 'buffer' which have not been searched before. |
| |
| See class spawn for the 'searchwindowsize' argument. |
| |
| If there is a match this returns the index of that string, and sets |
| 'start', 'end' and 'match'. Otherwise, returns -1.''' |
| |
| first_match = None |
| # 'freshlen' doesn't help here -- we cannot predict the |
| # length of a match, and the re module provides no help. |
| if searchwindowsize is None: |
| searchstart = 0 |
| else: |
| searchstart = max(0, len(buffer) - searchwindowsize) |
| for index, s in self._searches: |
| match = s.search(buffer, searchstart) |
| if match is None: |
| continue |
| n = match.start() |
| if first_match is None or n < first_match: |
| first_match = n |
| the_match = match |
| best_index = index |
| if first_match is None: |
| return -1 |
| self.start = first_match |
| self.match = the_match |
| self.end = self.match.end() |
| return best_index |