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