--- /dev/null
+package uk.org.distorted.tripe; package object peers {
+
+import java.io.{BufferedReader, File, FileReader, Reader};
+import java.net.{InetAddress, Inet4Address, Inet6Address,
+ UnknownHostException};
+
+import scala.collection.mutable.{HashMap, HashSet};
+import scala.concurrent.Channel;
+import scala.util.control.Breaks;
+import scala.util.matching.Regex;
+
+val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r;
+val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r;
+val RX_ASSGN = """(?x) ^
+ ([^\s:=] (?: [^:=]* [^\s:=])?)
+ \s* [:=] \s*
+ (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
+ (?: \s+ (?: [;\#].*)? )? $""".r;
+val RX_CONT = """(?x) ^ \s+
+ (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
+ (?: \s+ (?: [;\#].*)? )? $""".r;
+val RX_REF = """(?x) \$ \( ([^)]+) \)""".r;
+val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r;
+val RX_PARENT = """(?x) [^\s,]+""".r
+
+def with_cleaner[T](body: Cleaner => T): T = {
+ val cleaner = new Cleaner;
+ try { body(cleaner) }
+ finally { cleaner.cleanup(); }
+}
+
+class Cleaner {
+ var cleanups: List[() => Unit] = Nil;
+ def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
+ def cleanup() { cleanups foreach { _() } }
+}
+
+def lines(r: Reader) = new Traversable[String] {
+ val in: BufferedReader = new BufferedReader(r);
+ override def foreach[T](f: String => T) {
+ while (true) in.readLine match {
+ case null => return;
+ case line => f(line);
+ }
+ }
+}
+
+def thread(name: String, run: Boolean = true, daemon: Boolean = true)
+ (body: => Unit): Thread = {
+ val t = new Thread(new Runnable { override def run() { body } }, name);
+ t.setDaemon(daemon);
+ if (run) t.start();
+ t
+}
+
+object BulkResolver {
+ val BREAK = new Breaks;
+}
+
+class BulkResolver(val nthreads: Int = 8) {
+ import BulkResolver.BREAK._;
+ class Host(val name: String) {
+ var a4, a6: Seq[InetAddress] = Seq.empty;
+
+ def addaddr(a: InetAddress) { a match {
+ case _: Inet4Address => a4 +:= a;
+ case _: Inet6Address => a6 +:= a;
+ case _ => ();
+ } }
+
+ def get(flags: String): Seq[InetAddress] = {
+ var wanta4, wanta6, any, all = false;
+ var b = Seq.newBuilder[InetAddress];
+ for (ch <- flags) ch match {
+ case '*' => all = true;
+ case '4' => wanta4 = true; any = true;
+ case '6' => wanta6 = true; any = true;
+ case _ => ???
+ }
+ if (!any) { wanta4 = true; wanta6 = true; }
+ if (wanta4) b ++= a4;
+ if (wanta6) b ++= a6;
+ (all, b.result) match {
+ case (true, aa) => aa
+ case (false, aa@(Nil | Seq(_))) => aa
+ case (false, Seq(a, _*)) => Seq(a)
+ }
+ }
+ }
+ val ch = new Channel[Host];
+ val map = HashMap[String, Host]();
+ var preparing = true;
+
+ val workers = Array.tabulate(nthreads) { i =>
+ thread(s"resolver worker #$i") {
+ breakable {
+ while (true) {
+ val host = ch.read; if (host == null) break;
+println(s";; ${Thread.currentThread.getName} resolving `${host.name}'");
+ try {
+ for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a);
+ } catch { case e: UnknownHostException => () }
+ }
+ }
+println(s";; ${Thread.currentThread.getName} done'");
+ ch.write(null);
+ }
+ }
+
+ def prepare(name: String) {
+println(s";; prepare host `$name'");
+ assert(preparing);
+ if (!(map contains name)) {
+ val host = new Host(name);
+ map(name) = host;
+ ch.write(host);
+ }
+ }
+
+ def finish() {
+ assert(preparing);
+ preparing = false;
+ ch.write(null);
+ for (t <- workers) t.join();
+ }
+
+ def resolve(name: String, flags: String): Seq[InetAddress] =
+ map(name).get(flags);
+}
+
+def fmtpath(path: Seq[String]) =
+ path.reverse map { i => s"`$i'" } mkString " -> ";
+
+class ConfigSyntaxError(val file: File, val lno: Int, val msg: String)
+ extends Exception {
+ override def getMessage(): String = s"$file:$lno: $msg";
+}
+class MissingConfigSection(val sect: String) extends Exception {
+ override def getMessage(): String =
+ s"missing configuration section `$sect'";
+}
+class MissingConfigItem(val sect: String, val key: String,
+ val path: Seq[(String)]) extends Exception {
+ override def getMessage(): String = {
+ val msg = s"missing configuration item `$key' in section `$sect'";
+ if (path == Nil) msg
+ else msg + s" (wanted while expanding ${fmtpath(path)})"
+ }
+}
+class AmbiguousConfig(val key: String,
+ val v0: String, val p0: Seq[String],
+ val v1: String, val p1: Seq[String])
+ extends Exception {
+ override def getMessage(): String =
+ s"ambiguous answer resolving key `$key': " +
+ s"path ${fmtpath(p0)} yields `$v0', but ${fmtpath(p1)} yields `$v1'";
+}
+
+class ConfigCycle(val key: String, path: Seq[String]) extends Exception {
+ override def getMessage(): String =
+ s"found a cycle ${fmtpath(path)} looking up key `$key'";
+}
+class NoHostAddresses(val sect: String, val key: String, val host: String)
+ extends Exception {
+ override def getMessage(): String =
+ s"no addresses found for `$host' (key `$key' in section `$sect')";
+}
+
+object Config {
+ sealed abstract class ConfigCacheEntry;
+ case object StillLooking extends ConfigCacheEntry;
+ case object NotFound extends ConfigCacheEntry;
+ case class Found(value: String, path: Seq[String])
+ extends ConfigCacheEntry;
+}
+
+class Config { conf =>
+ import Config._;
+ class Section(val name: String) {
+ val itemmap = HashMap[String, String]();
+ val cache = HashMap[String, ConfigCacheEntry]();
+ override def toString: String = s"${getClass.getName}($name)";
+ def parents: Seq[Section] =
+ (itemmap.get("@inherit")
+ map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList }
+ getOrElse Nil);
+
+ def get_internal(key: String, path: Seq[String] = Nil):
+ Option[(String, Seq[String])] = {
+ val incpath = name +: path;
+
+ for (r <- cache.get(key)) r match {
+ case StillLooking => throw new ConfigCycle(key, incpath)
+ case NotFound => return None
+ case Found(v, p) => return Some((v, p ++ path));
+ }
+
+ for (v <- itemmap.get(key)) {
+ cache(key) = Found(v, Seq(name));
+ return Some((v, incpath));
+ }
+
+ cache(key) = StillLooking;
+
+ ((None: Option[(String, Seq[String])]) /: parents) { (st, parent) =>
+ parent.get_internal(key, incpath) match {
+ case None => st;
+ case newst@Some((v, p)) => st match {
+ case None => newst
+ case Some((vv, _)) if v == vv => st
+ case Some((vv, pp)) =>
+ throw new AmbiguousConfig(key, v, p, vv, pp)
+ }
+ }
+ } match {
+ case None => cache(key) = NotFound; None
+ case Some((v, p)) =>
+ cache(key) = Found(v, p dropRight path.length);
+ Some((v, p))
+ }
+ }
+
+ def get(key: String, resolve: Boolean = true,
+ path: Seq[String] = Nil): String = {
+ val v0 = key match {
+ case "name" => itemmap.getOrElse("name", name)
+ case _ => get_internal(key).
+ getOrElse(throw new MissingConfigItem(name, key, path))._1
+ }
+ expand(key, v0, resolve, path)
+ }
+
+ def expand(key: String, value: String, resolve: Boolean,
+ path: Seq[String]): String = {
+ val v1 = RX_REF.replaceAllIn(value, { m =>
+ Regex.quoteReplacement(get(m.group(1), resolve, path))
+ });
+ val v2 = if (!resolve) v1
+ else RX_RESOLVE.replaceAllIn(v1, { m =>
+ resolver.resolve(m.group(2), m.group(1)) match {
+ case Nil =>
+ throw new NoHostAddresses(name, key, m.group(2));
+ case addrs =>
+ Regex.quoteReplacement((addrs map { _.getHostAddress }).
+ mkString(" "));
+ }
+ })
+ v2
+ }
+
+ def items: Seq[String] = {
+ val b = Seq.newBuilder[String];
+ val seen = HashSet[String]();
+ val visiting = HashSet[String](name);
+ var stack = List(this);
+
+ while (stack != Nil) {
+ val sect = stack.head; stack = stack.tail;
+ for (k <- sect.itemmap.keys)
+ if (!(seen contains k)) { b += k; seen += k; }
+ for (p <- sect.parents)
+ if (!(visiting contains p.name))
+ { stack ::= p; visiting += p.name; }
+ }
+ b.result
+ }
+ }
+ val sectmap = new HashMap[String, Section];
+ def sections: Iterator[Section] = sectmap.values.iterator;
+ def section(name: String): Section =
+ sectmap.getOrElse(name, throw new MissingConfigSection(name));
+
+ val resolver = new BulkResolver;
+
+ def parseFile(path: File): this.type = {
+println(s";; parse ${path.getPath}");
+ with_cleaner { clean =>
+ val in = new FileReader(path); clean { in.close(); }
+
+ val lno = 1;
+ val b = new StringBuilder;
+ var key: String = null;
+ var sect: Section = null;
+ def flush() {
+ if (key != null) {
+ sect.itemmap(key) = b.result;
+println(s";; in `${sect.name}', set `$key' to `${b.result}'");
+ b.clear();
+ key = null;
+ }
+ }
+ for (line <- lines(in)) line match {
+ case RX_COMMENT() =>
+ ();
+ case RX_GRPHDR(grp) =>
+ flush();
+ sect = sectmap.getOrElseUpdate(grp, new Section(grp));
+ case RX_CONT(v) =>
+ if (key == null) {
+ throw new ConfigSyntaxError(
+ path, lno, "no config value to continue");
+ }
+ b += '\n'; b ++= v;
+ case RX_ASSGN(k, v) =>
+ if (sect == null) {
+ throw new ConfigSyntaxError(
+ path, lno, "no active section to update");
+ }
+ flush();
+ key = k; b ++= v;
+ case _ =>
+ throw new ConfigSyntaxError(path, lno, "incomprehensible line");
+ }
+ flush();
+ }
+ this
+ }
+ def parse(path: File): this.type = {
+ if (!path.isDirectory) parseFile(path);
+ else for {
+ f <- path.listFiles sortBy { _.getName };
+ name = f.getName;
+ if name.length > 0;
+ tail = name(name.length - 1);
+ if tail != '#' && tail != '~'
+ } parseFile(f);
+ this
+ }
+ def parse(path: String): this.type = parse(new File(path));
+
+ def analyse() {
+println(";; resolving all...");
+ for ((_, sect) <- sectmap) {
+println(s";; resolving in section `${sect.name}'...");
+ for (key <- sect.items) {
+println(s";; resolving in key `$key'...");
+ val mm = RX_RESOLVE.findAllIn(sect.get(key, false));
+ for (host <- mm) { resolver.prepare(mm.group(2)); }
+ }
+ }
+ resolver.finish();
+
+ def dumpsect(sect: Section) {
+ for (k <- sect.items.filterNot(_.startsWith("@")).sorted)
+ println(s";; `$k' -> `${sect.get(k)}'")
+ }
+ for (sect <- sectmap.values.toSeq sortBy { _.name })
+ if (sect.name.startsWith("@")) ();
+ else if (sect.name.startsWith("$")) {
+ println(s";; special section `${sect.name}'");
+ dumpsect(sect);
+ } else {
+ println(s";; peer section `${sect.name}'");
+ dumpsect(sect);
+ }
+ }
+}
+
+}