X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe/blobdiff_plain/6005ef9bfba49124a25825a5b044d4f4cbf02792..26936c8341691d67655a055956656f2506d53a63:/peerdb/tripe-newpeers.in diff --git a/peerdb/tripe-newpeers.in b/peerdb/tripe-newpeers.in index d7205845..a40d438f 100644 --- a/peerdb/tripe-newpeers.in +++ b/peerdb/tripe-newpeers.in @@ -75,11 +75,12 @@ class BulkResolver (object): def prepare(me, host): """Prime the resolver to resolve the name HOST.""" - me._resolvers[host] = M.SelResolveByName \ - (host, - lambda name, alias, addr: - me._resolved(host, addr[0]), - lambda: me._resolved(host, None)) + if host not in me._resolvers: + me._resolvers[host] = M.SelResolveByName \ + (host, + lambda name, alias, addr: + me._resolved(host, addr[0]), + lambda: me._resolved(host, None)) def run(me): """Run the background DNS resolver until it's finished.""" @@ -109,6 +110,34 @@ r_ref = RX.compile(r'\$\(([^)]+)\)') ## Match a $[HOST] name resolution reference; group 1 is the HOST. r_resolve = RX.compile(r'\$\[([^]]+)\]') +def _fmt_path(path): + return ' -> '.join(["`%s'" % hop for hop in path]) + +class AmbiguousOptionError (Exception): + def __init__(me, key, patha, vala, pathb, valb): + me.key = key + me.patha, me.vala = patha, vala + me.pathb, me.valb = pathb, valb + def __str__(me): + return "Ambiguous answer resolving key `%s': " \ + "path %s yields `%s' but %s yields `%s'" % \ + (me.key, _fmt_path(me.patha), me.vala, _fmt_path(me.pathb), me.valb) + +class InheritanceCycleError (Exception): + def __init__(me, key, path): + me.key = key + me.path = path + def __str__(me): + return "Found a cycle %s looking up key `%s'" % \ + (_fmt_path(me.path), me.key) + +class MissingKeyException (Exception): + def __init__(me, sec, key): + me.sec = sec + me.key = key + def __str__(me): + return "Key `%s' not found in section `%s'" % (me.key, me.sec) + class MyConfigParser (CP.RawConfigParser): """ A more advanced configuration parser. @@ -179,44 +208,94 @@ class MyConfigParser (CP.RawConfigParser): This version of the method properly handles the @inherit key. """ - return CP.RawConfigParser.has_option(me, sec, key) or \ - (CP.RawConfigParser.has_option(me, sec, '@inherit') and - me.has_option(CP.RawConfigParser.get(me, sec, '@inherit'), key)) + return key == 'name' or me._get(sec, key)[0] is not None - def _get(me, basesec, sec, key, resolvep): + def _get(me, sec, key, map = None, path = None): """ Low-level option-fetching method. Fetch the value for the named KEY from section SEC, or maybe - (recursively) the section which SEC inherits from. + (recursively) a section which SEC inherits from. - The result is expanded, by _expend; RESOLVEP is passed to _expand to - control whether $[...] should be expanded in the result. + Returns a pair VALUE, PATH. The value is not expanded; nor do we check + for the special `name' key. The caller is expected to do these things. + Returns None if no value could be found. + """ - The BASESEC is the section for which the original request was made. This - will be different from SEC if we're recursing up the inheritance chain. + ## If we weren't given a memoization map or path, then we'd better make + ## one. + if map is None: map = {} + if path is None: path = [] - We also provide the default value for `name' here. - """ + ## If we've been this way before on another pass through then return the + ## value we found then. If we're still thinking about it then we've + ## found a cycle. + path.append(sec) + try: + threadp, value = map[sec] + except KeyError: + pass + else: + if threadp: + raise InheritanceCycleError, (key, path) + + ## See whether the answer is ready waiting for us. try: - raw = CP.RawConfigParser.get(me, sec, key) + v = CP.RawConfigParser.get(me, sec, key) except CP.NoOptionError: - if key == 'name': - raw = basesec - elif CP.RawConfigParser.has_option(me, sec, '@inherit'): - raw = me._get(basesec, - CP.RawConfigParser.get(me, sec, '@inherit'), - key, - resolvep) - else: - raise - return me._expand(basesec, raw, resolvep) + pass + else: + p = path[:] + path.pop() + return v, p + + ## No, apparently, not. Find out our list of parents. + try: + parents = CP.RawConfigParser.get(me, sec, '@inherit').\ + replace(',', ' ').split() + except CP.NoOptionError: + parents = [] + + ## Initially we have no idea. + value = None + winner = None + + ## Go through our parents and ask them what they think. + map[sec] = True, None + for p in parents: + + ## See whether we get an answer. If not, keep on going. + v, pp = me._get(p, key, map, path) + if v is None: continue + + ## If we got an answer, check that it matches any previous ones. + if value is None: + value = v + winner = pp + elif value != v: + raise AmbiguousOptionError, (key, winner, value, pp, v) + + ## That's the best we could manage. + path.pop() + map[sec] = False, value + return value, winner def get(me, sec, key, resolvep = True): """ Retrieve the value of KEY from section SEC. """ - return me._get(sec, sec, key, resolvep) + + ## Special handling for the `name' key. + if key == 'name': + try: value = CP.RawConfigParser.get(me, sec, key) + except CP.NoOptionError: value = sec + else: + value, _ = me._get(sec, key) + if value is None: + raise MissingKeyException, (sec, key) + + ## Expand the value and return it. + return me._expand(sec, value, resolvep) def items(me, sec, resolvep = True): """ @@ -224,17 +303,30 @@ class MyConfigParser (CP.RawConfigParser): This extends the default method by handling the inheritance chain. """ + + ## Initialize for a depth-first walk of the inheritance graph. d = {} + visited = {} basesec = sec - while sec: - next = None + stack = [sec] + + ## Visit nodes, collecting their keys. Don't believe the values: + ## resolving inheritance is too hard to do like this. + while stack: + sec = stack.pop() + if sec in visited: continue + visited[sec] = True + for key, value in CP.RawConfigParser.items(me, sec): - if key == '@inherit': - next = value - elif not key.startswith('@') and key not in d: - d[key] = me._expand(basesec, value, resolvep) - sec = next - return d.items() + if key == '@inherit': stack += value.replace(',', ' ').split() + else: d[key] = None + + ## Now collect the values for the known keys, one by one. + items = [] + for key in d: items.append((key, me.get(basesec, key, resolvep))) + + ## And we're done. + return items ###-------------------------------------------------------------------------- ### Command-line handling. @@ -302,7 +394,7 @@ def output(conf, cdb): This is where the special `user' and `auto' database entries get set. """ auto = [] - for sec in conf.sections(): + for sec in sorted(conf.sections()): if sec.startswith('@'): continue elif sec.startswith('$'): @@ -315,7 +407,7 @@ def output(conf, cdb): if conf.has_option(sec, 'user'): cdb.add('U%s' % conf.get(sec, 'user'), sec) url = M.URLEncode(laxp = True, semip = True) - for key, value in conf.items(sec): + for key, value in sorted(conf.items(sec), key = lambda (k, v): k): if not key.startswith('@'): url.encode(key, ' '.join(M.split(value)[0])) cdb.add(label, url.result)