chiark / gitweb /
wip
[tripe-android] / peers.scala
diff --git a/peers.scala b/peers.scala
new file mode 100644 (file)
index 0000000..931c8f4
--- /dev/null
@@ -0,0 +1,359 @@
+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);
+      }
+  }
+}
+
+}