diff --git a/pcl_expect/__init__.py b/pcl_expect/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d4dd29448dc7f454130015d910b7049ce9f9d111 --- /dev/null +++ b/pcl_expect/__init__.py @@ -0,0 +1,410 @@ +"""An extensible expect module with more expect-like feeling. + +FIXME: more doc needed. +""" + +import sets +import os +import re +import errno +import select + +__all__ = [ + "timeout", + "timeout_raises_exception", + "RE", + "EOF", + "TIMEOUT", + "CONT", + "Timeout", + "BadArgs", + "expectable", + "controller", + "expect_after", + "expect_before", + "expect", + ] + +# Default timeout, in seconds, as a floating point number. +# The user is supposed to change this as needed. +timeout = 10.0 + +# If a timeout occurs and no timeout handler is specified, should an +# exception be raised, or should the code just continue (TCL-style)? +# Default is to raise an exception. The user is supposed to change +# this as needed. +timeout_raises_exception = True + +# Constants that identify actions for expect_after and expect_before. +RE = 0 +EOF = 1 +TIMEOUT = 2 + +# Return values from callbacks. +CONT = 3 + +class Timeout(Exception): pass +class BadArgs(Exception): pass + +_expect_before = [] +_expect_after = [] + +def debug(s): + if 0: + sys.stderr.write("pcl-expect: %s\n" % s) + +class expectable: + def __init__(self, fileno): + self.__fileno = fileno + self.__buffer = "" + self.__eof_seen = False + + def fileno(self): + return self.__fileno + + def _read(self): + try: + s = os.read(self.fileno(), 8192) + except OSError, e: + if e.errno == errno.EIO: + debug("got EIO from fd %d" % self.fileno()) + s = "" + else: + raise + if s == "": + debug("got eof from fd %d" % self.fileno()) + return "", True + else: + return s, False + + def fill_buffer(self): + if not self.__eof_seen: + s, eof = self._read() + if eof: + self.__eof_seen = True + else: + debug("got %d bytes from fd %d" % (len(s), self.fileno())) + self.__buffer = self.__buffer + s + else: + debug("eof already seen on fd %d; not reading" % self.fileno()) + return self.__eof_seen + + def eof(self): + """Return True if end-of-file has been seen. + + There might still be pending data in the buffer, though. + """ + return self.__eof_seen + + def buffer(self): + return self.__buffer + + def remove_first(self, n): + self.__buffer = self.__buffer[n:] + + +class controller: + def __init__(self, timeout = None): + self.__first = True + self.__inputs = sets.Set() + self.__readable = sets.Set() + self.__done = False + self.__timeout = timeout + self.__timeout_active = False + self.__acted = False + self.__all_inputs_seen = False + debug("START") + + def loop(self): + debug("at top of loop") + if self.__done: + debug("done -- exiting expect") + return False + + if self.__first: + self.__first = False + debug("first time round") + self.__acted = False + if self.__expect_before(): + return False + return True + elif not self.__acted: + debug("no action taken during previous loop") + self.__all_inputs_seen = True + if self.__expect_after(): + return False + + self.__acted = False + + # Nothing handled the timeout event. + if self.__timeout_active: + debug("Unhandled timeout") + if timeout_raises_exception: + raise Timeout() + else: + return False + + r = ", ".join([str(x.fileno()) for x in self.__inputs]) + if not self.__all_inputs_seen: + t = 0 + elif self.__timeout == None: + t = timeout + else: + t = self.__timeout + if t > 0: + debug("Waiting for input on %s" % r) + else: + debug("Polling for input on %s" % r) + (r, w, e) = select.select([x for x in self.__inputs], [], [], t) + self.__readable = sets.Set(r) + if len(self.__readable) == 0: + if self.__all_inputs_seen: + debug("Processing timeout event") + self.__timeout_active = True + else: + debug("Input available") + if self.__expect_before(): + return False + return True + + + def __fill_buffer(self, exp): + if exp in self.__readable: + debug("reading from fd %d" % exp.fileno()) + self.__readable.remove(exp) + if exp.fill_buffer(): + debug("got eof on fd %d" % exp.fileno()) + self.__inputs.discard(exp) + + def re(self, exp, regexp): + """Implement "when re foo, foo_re". + + EXP is an event source that adheres to the expectable API. + REGEXP is a regular expression to match. This can be a string + (that will be compiled using re.compile) or a compiled regexp + object. + + Return true if a match was found. The match attribute of EXP + will be set to the result of the match. The matching string + will have been removed from the buffer of EXP upon return. + """ + + if self.__acted: + return False + + if self.__done: + raise "foo! me bad!" + + self.__fill_buffer(exp) + + debug("does %s match %s (fd %d)?" % ( + repr(exp.buffer()), repr(regexp), exp.fileno())) + + # Doing this compilation again and again could be a problem. + # I rely on the cache in module re. I hope it exists... + if isinstance(regexp, basestring): + regexp = re.compile(regexp) + + match = regexp.search(exp.buffer()) + if match != None: + debug("yes") + exp.match = match + exp.remove_first(match.end()) + self.__done = True + self.__acted = True + return True + else: + debug("no") + if not exp.eof(): + debug("adding fd %d to rd-set" % exp.fileno()) + self.__inputs.add(exp) + return False + + def eof(self, exp): + """Implement "when eof foo". + """ + if self.__acted: + return False + + if self.__done: + raise "foo! me bad!" + + self.__fill_buffer(exp) + + if exp.eof(): + debug("eof seen on fd %d" % exp.fileno()) + exp.match = exp.buffer() + exp.remove_first(len(exp.match)) + self.__done = True + self.__acted = True + return True + else: + debug("no eof on fd %d" % exp.fileno()) + debug("adding fd %d to rd-set" % exp.fileno()) + self.__inputs.add(exp) + return False + + def timeout(self): + """Implement "when timeout". + + Return true if a timeout has occured. + """ + + if self.__acted: + return False + + if self.__done: + raise "foo! me bad!" + + if self.__timeout_active: + debug("eof pending") + self.__timeout_active = False + self.__done = True + self.__acted = True + return True + else: + debug("no eof pending") + return False + + def cont(self): + debug("cont called") + self.__done = False + self.__first = True + # Don't touch self.__acted + + def __expect_before(self): + debug("running expect_before patterns...") + ret = self.__run_expectations(_expect_before) + debug("done running expect_before patterns: %s." % ret) + return ret + + def __expect_after(self): + debug("running expect_after patterns...") + ret = self.__run_expectations(_expect_after) + debug("done running expect_after patterns: %s." % ret) + return ret + + def __run_expectations(self, expectations): + for pattern in expectations: + debug("running pattern %s" % (pattern,)) + if pattern[0] == RE: + (cmd, exp, regexp, callback) = pattern + if self.re(exp, regexp): + if callback(exp) == CONT: + self.cont() + return False + else: + return True + elif pattern[0] == EOF: + (cmd, exp, callback) = pattern + if self.eof(exp): + if callback(exp) == CONT: + self.cont() + return False + else: + return True + elif pattern[0] == TIMEOUT: + (cmd, callback) = pattern + if self.timeout(): + if callback() == CONT: + self.cont() + return False + else: + return True + else: + raise BadArgs + debug("no match") + return False + + +def expect_after(expectations): + """Set a list of expect patterns to run if no other matches are found. + + The argument is a list of tuples. These tuples are understood: + + (RE, exp, regexp, callback): If output from EXP matches REGEXP, + call CALLBACK. + (EOF, exp, callback): If end-of-file is reached on EXP, call CALLBACK. + (TIMEOUT, callback): If a timeout occurs, call CALLBACK. + + The callback function is called with EXP as argument (or, for + timeout, with no argument). It should return None or CONT. + Returning CONT from a callback means that the expect statement + should start again. + """ + global _expect_after + + __validate_expectations(expectations) + _expect_after = expectations + debug("Added expect_after patterns") + +def expect_before(expectations): + """Set a list of expect patterns to run before looking at other matches. + + See expect_after for more info. + """ + global _expect_before + + __validate_expectations(expectations) + _expect_before = expectations + debug("Added expect_before patterns") + + +def __validate_expectations(expectations): + for pattern in expectations: + if pattern[0] == RE: + (cmd, exp, regexp, callback) = pattern + elif pattern[0] == EOF: + (cmd, exp, callback) = pattern + elif pattern[0] == TIMEOUT: + (cmd, callback) = pattern + else: + raise BadArgs + +def expect(expectations): + x = controller() + while x.loop(): + for ix in range(len(expectations)): + pattern = expectations[ix] + if pattern[0] == RE: + if len(pattern) == 3: + (cmd, exp, regexp) = pattern + callback = None + elif len(pattern) == 4: + (cmd, exp, regexp, callback) = pattern + else: + raise BadArgs + if x.re(exp, regexp): + if callback is not None and callback(exp) == CONT: + x.cont() + break + else: + return ix, exp + elif pattern[0] == EOF: + if len(pattern) == 2: + (cmd, exp) = pattern + callback = None + elif len(pattern) == 3: + (cmd, exp, callback) = pattern + else: + raise BadArgs + if x.eof(exp): + if callback is not None and callback(exp) == CONT: + x.cont() + break + else: + return ix, exp + elif pattern[0] == TIMEOUT: + if len(pattern) == 1: + cmd = pattern[0] + elif len(pattern) == 2: + (cmd, callback) = pattern + else: + raise BadArgs + if x.timeout(): + if callback is not None and callback() == CONT: + x.cont() + break + else: + return ix, None + return None, None