Commit | Line | Data |
---|---|---|
8eabb4ff MW |
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 | } |