# -*- coding: utf-8 -*- __docformat__ = 'restructuredtext' from category import Category from pattern import Pattern from confparse import Conf, WhenSpec, OnSpec, ExecSpec import setcross class StateMachine: """ The state machine contains a list of dependencies between states, and a list of states which are "up." """ def __init__(self, conf_str = None, conf_file = None): """ Create a new state machine. `conf_file` if specified is a file to load configuration from. `conf_str` if specified is a configuration string. If both are specified `conf_file` takes precedence, and `conf_str` is used on error. """ self.up = set() self.wanted = set() self.deps = [] self.event_triggers = [] if conf_file != None: try: file = open(conf_file) conf_str = file.read() file.close() except: pass if conf_str != None: self.load_conf(conf_str) def load_conf(self, str): """ Load some string configuration into this state machine. """ conf = Conf(str) for block in conf.propmatches: self.load_conf_block(block) def load_conf_block(self, block): """ Load a single block of text configuration. `block` is the parse tree node at the root of the block. """ cat = block.pattern.to_category() for prop in block.properties: prop = prop.spec if prop.__class__ == WhenSpec: self.deps.append((cat, prop.waiting_for.to_category())) elif prop.__class__ == OnSpec: self.event_triggers.append( (prop.waiting_for.to_category(), cat)) else: # prop.__class__ == ExecSpec pass #TODO: Exec support def emit(self, event): """ Emit an event. """ retval = False for evpat, cat in self.event_triggers: if event.subset_of(evpat): retval |= self.bring_up(cat.intersect_args(**event.args)) return retval def bring_up(self, cat, wanted=True): """ Move states in the given Category `cat` from down to up. Don't check to see if the category makes sense first. """ found = None for (match, dependency) in self.get_applicable_deps(cat): res = self.get_satisfied_states(match, dependency) if len(res) == 0: return False if found == None: found = [res] else: found.append(res) if found == None: if self.add_hold(cat, wanted): sm.emit(Category("ε")) return True to_add = self.cat_cross(found) added = False for x in to_add: added = self.add_hold(x, wanted) or added if added: sm.emit(Category("ε")) return True def cat_cross(self, found): """ Given a list of sets, where each set contains Category objects, return a set of all categories that can be made by intersecting one element from each set. """ to_add = set() for tup in setcross.cross(*found): orig = tup while len(tup) > 1: newtup = (tup[0].intersect(tup[1]),) if newtup[0] == None: tup = () break tup = newtup + tup[2:len(tup)] if len(tup) == 0 or tup[0] == None: continue to_add.add(tup[0]) return to_add def add_hold(self, cat, wanted): """ Add a hold to a state. Does not check dependencies. Returns False if the state was up, True if it wasn't. """ for x in self.up: if cat.subset_of(x): return False if x.subset_of(cat): self.up.remove(x) self.up.add(cat) return True self.up.add(cat) if wanted: self.wanted.add(cat) return True def bring_down(self, cat, rec=False): """ Bring a currently "up" state down. """ to_drop = set([ x for x in self.up if x.subset_of(cat) ]) if None in to_drop: to_drop.remove(None) if len(to_drop) == 0: return False for (dependent, dependency) in self.deps: match = set([dependency.intersect(x) for x in to_drop]) if None in match: match.remove(None) if match != None: for item in match: self.bring_down(dependent.fill(item.args), rec=True) self.up -= to_drop self.wanted -= to_drop if not rec: self.cleanup_states() def cleanup_states(self): """ Remove unwanted states """ new_up = self.wanted.copy() addition = set([1]) while len(addition): addition = set() for s in addition: for x in self.get_applicable_deps(s): for y in self.up: if y.subset_of(x): addition.add(y) new_up |= addition self.up = new_up def get_satisfied_states(self, dependents, dependencies): """ Given that states in `dependents` depend on states in `dependencies`, return a new Category that contains only the states in `dependents` that could match states in `dependencies`. """ retval = set() for cat in self.up: if dependencies.superset_of(cat): retval.add(dependents.intersect_args(**cat.args)) if None in retval: retval.remove(None) return retval | dependents.inverse_set() def get_applicable_deps(self, cat): """ Find dependencies that might apply to members of `cat` """ retval = [] for (x, y) in self.deps: un = cat.intersect(x) if un != None: retval.append((un, y.fill(un.args))) return retval def __str__(self): return "\n".join(["%s" % k for k in self.up]) def __repr__(self): return str(self) def m(*args): return Pattern(True, *args) # The "m" reads "match", so m("foo", "bar") reads 'match foo and bar' def nm(*args): return Pattern(False, *args) # Reads as "don't match." See above def any(): return nm() # Match anything. Implementation reads "don't match nothing" if __name__ == "__main__": sm = StateMachine(conf_str = """ mounted(type: nfs): when network_up mounted(type: ! nfs): when found_disk mounted: when vol_conf mounted(auto: 1): auto vol_conf(src: fstabd) on fstab_line """) sm.bring_up(Category("network_up")) sm.bring_up(Category("found_disk", uuid=m("d3adb3ef"), devname=m("/dev/sda"), label=m("myroot"))) sm.bring_up(Category("mounted")) print sm print "--" sm.emit(Category("fstab_line", label=m("myroot"), type=m("ext3"), mountpoint=m("/"), auto=m("1"))) sm.emit(Category("fstab_line", devname=m("foosrv.com:/vol/home"), type=m("nfs"), mountpoint=m("/home"), auto=m("0"))) sm.emit(Category("fstab_line", devname=m("foosrv.com:/vol/beefs"), type=m("nfs"), mountpoint=m("/beefs"), auto=m("0"))) print sm print "--" sm.bring_up(Category("mounted")) print sm print "--" sm.bring_down(Category("network_up")) print sm