chiark / gitweb /
931c8f4d27af1faaf63561c8d666b2c42f7fe089
[tripe-android] / peers.scala
1 package uk.org.distorted.tripe; package object peers {
2
3 import java.io.{BufferedReader, File, FileReader, Reader};
4 import java.net.{InetAddress, Inet4Address, Inet6Address,
5                  UnknownHostException};
6
7 import scala.collection.mutable.{HashMap, HashSet};
8 import scala.concurrent.Channel;
9 import scala.util.control.Breaks;
10 import scala.util.matching.Regex;
11
12 val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r;
13 val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r;
14 val RX_ASSGN = """(?x) ^
15         ([^\s:=] (?: [^:=]* [^\s:=])?)
16         \s* [:=] \s*
17         (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
18         (?: \s+ (?: [;\#].*)? )? $""".r;
19 val RX_CONT = """(?x) ^ \s+
20         (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
21         (?: \s+ (?: [;\#].*)? )? $""".r;
22 val RX_REF = """(?x) \$ \( ([^)]+) \)""".r;
23 val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r;
24 val RX_PARENT = """(?x) [^\s,]+""".r
25
26 def with_cleaner[T](body: Cleaner => T): T = {
27   val cleaner = new Cleaner;
28   try { body(cleaner) }
29   finally { cleaner.cleanup(); }
30 }
31
32 class Cleaner {
33   var cleanups: List[() => Unit] = Nil;
34   def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
35   def cleanup() { cleanups foreach { _() } }
36 }
37
38 def lines(r: Reader) = new Traversable[String] {
39   val in: BufferedReader = new BufferedReader(r);
40   override def foreach[T](f: String => T) {
41     while (true) in.readLine match {
42       case null => return;
43       case line => f(line);
44     }
45   }
46 }
47
48 def thread(name: String, run: Boolean = true, daemon: Boolean = true)
49           (body: => Unit): Thread = {
50   val t = new Thread(new Runnable { override def run() { body } }, name);
51   t.setDaemon(daemon);
52   if (run) t.start();
53   t
54 }
55
56 object BulkResolver {
57   val BREAK = new Breaks;
58 }
59
60 class BulkResolver(val nthreads: Int = 8) {
61   import BulkResolver.BREAK._;
62   class Host(val name: String) {
63     var a4, a6: Seq[InetAddress] = Seq.empty;
64
65     def addaddr(a: InetAddress) { a match {
66       case _: Inet4Address => a4 +:= a;
67       case _: Inet6Address => a6 +:= a;
68       case _ => ();
69     } }
70
71     def get(flags: String): Seq[InetAddress] = {
72       var wanta4, wanta6, any, all = false;
73       var b = Seq.newBuilder[InetAddress];
74       for (ch <- flags) ch match {
75         case '*' => all = true;
76         case '4' => wanta4 = true; any = true;
77         case '6' => wanta6 = true; any = true;
78         case _ => ???
79       }
80       if (!any) { wanta4 = true; wanta6 = true; }
81       if (wanta4) b ++= a4;
82       if (wanta6) b ++= a6;
83       (all, b.result) match {
84         case (true, aa) => aa
85         case (false, aa@(Nil | Seq(_))) => aa
86         case (false, Seq(a, _*)) => Seq(a)
87       }
88     }
89   }
90   val ch = new Channel[Host];
91   val map = HashMap[String, Host]();
92   var preparing = true;
93
94   val workers = Array.tabulate(nthreads) { i =>
95     thread(s"resolver worker #$i") {
96       breakable {
97         while (true) {
98           val host = ch.read; if (host == null) break;
99 println(s";; ${Thread.currentThread.getName} resolving `${host.name}'");
100           try {
101             for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a);
102           } catch { case e: UnknownHostException => () }
103         }
104       }
105 println(s";; ${Thread.currentThread.getName} done'");
106       ch.write(null);
107     }
108   }
109
110   def prepare(name: String) {
111 println(s";; prepare host `$name'");
112     assert(preparing);
113     if (!(map contains name)) {
114       val host = new Host(name);
115       map(name) = host;
116       ch.write(host);
117     }
118   }
119
120   def finish() {
121     assert(preparing);
122     preparing = false;
123     ch.write(null);
124     for (t <- workers) t.join();
125   }
126
127   def resolve(name: String, flags: String): Seq[InetAddress] =
128     map(name).get(flags);
129 }
130
131 def fmtpath(path: Seq[String]) =
132   path.reverse map { i => s"`$i'" } mkString " -> ";
133
134 class ConfigSyntaxError(val file: File, val lno: Int, val msg: String)
135         extends Exception {
136   override def getMessage(): String = s"$file:$lno: $msg";
137 }
138 class MissingConfigSection(val sect: String) extends Exception {
139   override def getMessage(): String =
140     s"missing configuration section `$sect'";
141 }
142 class MissingConfigItem(val sect: String, val key: String,
143                         val path: Seq[(String)]) extends Exception {
144   override def getMessage(): String = {
145     val msg = s"missing configuration item `$key' in section `$sect'";
146     if (path == Nil) msg
147     else msg + s" (wanted while expanding ${fmtpath(path)})"
148   }
149 }
150 class AmbiguousConfig(val key: String,
151                       val v0: String, val p0: Seq[String],
152                       val v1: String, val p1: Seq[String])
153         extends Exception {
154   override def getMessage(): String =
155     s"ambiguous answer resolving key `$key': " +
156     s"path ${fmtpath(p0)} yields `$v0', but ${fmtpath(p1)} yields `$v1'";
157 }
158
159 class ConfigCycle(val key: String, path: Seq[String]) extends Exception {
160   override def getMessage(): String =
161     s"found a cycle ${fmtpath(path)} looking up key `$key'";
162 }
163 class NoHostAddresses(val sect: String, val key: String, val host: String)
164         extends Exception {
165   override def getMessage(): String =
166     s"no addresses found for `$host' (key `$key' in section `$sect')";
167 }
168
169 object Config {
170   sealed abstract class ConfigCacheEntry;
171   case object StillLooking extends ConfigCacheEntry;
172   case object NotFound extends ConfigCacheEntry;
173   case class Found(value: String, path: Seq[String])
174     extends ConfigCacheEntry;
175 }
176
177 class Config { conf =>
178   import Config._;
179   class Section(val name: String) {
180     val itemmap = HashMap[String, String]();
181     val cache = HashMap[String, ConfigCacheEntry]();
182     override def toString: String = s"${getClass.getName}($name)";
183     def parents: Seq[Section] =
184       (itemmap.get("@inherit")
185        map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList }
186        getOrElse Nil);
187
188     def get_internal(key: String, path: Seq[String] = Nil):
189               Option[(String, Seq[String])] = {
190       val incpath = name +: path;
191
192       for (r <- cache.get(key)) r match {
193         case StillLooking => throw new ConfigCycle(key, incpath)
194         case NotFound => return None
195         case Found(v, p) => return Some((v, p ++ path));
196       }
197
198       for (v <- itemmap.get(key)) {
199         cache(key) = Found(v, Seq(name));
200         return Some((v, incpath));
201       }
202
203       cache(key) = StillLooking;
204
205       ((None: Option[(String, Seq[String])]) /: parents) { (st, parent) =>
206         parent.get_internal(key, incpath) match {
207           case None => st;
208           case newst@Some((v, p)) => st match {
209             case None => newst
210             case Some((vv, _)) if v == vv => st
211             case Some((vv, pp)) =>
212               throw new AmbiguousConfig(key, v, p, vv, pp)
213           }
214         }
215       } match {
216         case None => cache(key) = NotFound; None
217         case Some((v, p)) =>
218           cache(key) = Found(v, p dropRight path.length);
219           Some((v, p))
220       }
221     }
222
223     def get(key: String, resolve: Boolean = true,
224             path: Seq[String] = Nil): String = {
225       val v0 = key match {
226         case "name" => itemmap.getOrElse("name", name)
227         case _ => get_internal(key).
228           getOrElse(throw new MissingConfigItem(name, key, path))._1
229       }
230       expand(key, v0, resolve, path)
231     }
232
233     def expand(key: String, value: String, resolve: Boolean,
234                path: Seq[String]): String = {
235       val v1 = RX_REF.replaceAllIn(value, { m =>
236         Regex.quoteReplacement(get(m.group(1), resolve, path))
237       });
238       val v2 = if (!resolve) v1
239                else RX_RESOLVE.replaceAllIn(v1, { m =>
240                  resolver.resolve(m.group(2), m.group(1)) match {
241                    case Nil =>
242                      throw new NoHostAddresses(name, key, m.group(2));
243                    case addrs =>
244                      Regex.quoteReplacement((addrs map { _.getHostAddress }).
245                                             mkString(" "));
246                  }
247                })
248       v2
249     }
250
251     def items: Seq[String] = {
252       val b = Seq.newBuilder[String];
253       val seen = HashSet[String]();
254       val visiting = HashSet[String](name);
255       var stack = List(this);
256
257       while (stack != Nil) {
258         val sect = stack.head; stack = stack.tail;
259         for (k <- sect.itemmap.keys)
260           if (!(seen contains k)) { b += k; seen += k; }
261         for (p <- sect.parents)
262           if (!(visiting contains p.name))
263             { stack ::= p; visiting += p.name; }
264       }
265       b.result
266     }
267   }
268   val sectmap = new HashMap[String, Section];
269   def sections: Iterator[Section] = sectmap.values.iterator;
270   def section(name: String): Section =
271     sectmap.getOrElse(name, throw new MissingConfigSection(name));
272
273   val resolver = new BulkResolver;
274
275   def parseFile(path: File): this.type = {
276 println(s";; parse ${path.getPath}");
277     with_cleaner { clean =>
278       val in = new FileReader(path); clean { in.close(); }
279
280       val lno = 1;
281       val b = new StringBuilder;
282       var key: String = null;
283       var sect: Section = null;
284       def flush() {
285         if (key != null) {
286           sect.itemmap(key) = b.result;
287 println(s";; in `${sect.name}', set `$key' to `${b.result}'");
288           b.clear();
289           key = null;
290         }
291       }
292       for (line <- lines(in)) line match {
293         case RX_COMMENT() =>
294           ();
295         case RX_GRPHDR(grp) =>
296           flush();
297           sect = sectmap.getOrElseUpdate(grp, new Section(grp));
298         case RX_CONT(v) =>
299           if (key == null) {
300             throw new ConfigSyntaxError(
301               path, lno, "no config value to continue");
302           }
303           b += '\n'; b ++= v;
304         case RX_ASSGN(k, v) =>
305           if (sect == null) {
306             throw new ConfigSyntaxError(
307               path, lno, "no active section to update");
308           }
309           flush();
310           key = k; b ++= v;
311         case _ =>
312           throw new ConfigSyntaxError(path, lno, "incomprehensible line");
313       }
314       flush();
315     }
316     this
317   }
318   def parse(path: File): this.type = {
319     if (!path.isDirectory) parseFile(path);
320     else for {
321       f <- path.listFiles sortBy { _.getName };
322       name = f.getName;
323       if name.length > 0;
324       tail = name(name.length - 1);
325       if tail != '#' && tail != '~'
326     } parseFile(f);
327     this
328   }
329   def parse(path: String): this.type = parse(new File(path));
330
331   def analyse() {
332 println(";; resolving all...");
333     for ((_, sect) <- sectmap) {
334 println(s";; resolving in section `${sect.name}'...");
335       for (key <- sect.items) {
336 println(s";;    resolving in key `$key'...");
337         val mm = RX_RESOLVE.findAllIn(sect.get(key, false));
338         for (host <- mm) { resolver.prepare(mm.group(2)); }
339       }
340     }
341     resolver.finish();
342
343     def dumpsect(sect: Section) {
344       for (k <- sect.items.filterNot(_.startsWith("@")).sorted)
345         println(s";;    `$k' -> `${sect.get(k)}'")
346     }
347     for (sect <- sectmap.values.toSeq sortBy { _.name })
348       if (sect.name.startsWith("@")) ();
349       else if (sect.name.startsWith("$")) {
350         println(s";; special section `${sect.name}'");
351         dumpsect(sect);
352       } else {
353         println(s";; peer section `${sect.name}'");
354         dumpsect(sect);
355       }
356   }
357 }
358
359 }