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 | ||
8eabb4ff MW |
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") { | |
c8292b34 MW |
66 | loopUnit { exit => |
67 | val host = ch.read; if (host == null) exit; | |
8eabb4ff | 68 | println(s";; ${Thread.currentThread.getName} resolving `${host.name}'"); |
c8292b34 MW |
69 | try { |
70 | for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a); | |
71 | } catch { case e: UnknownHostException => () } | |
8eabb4ff MW |
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}"); | |
c8292b34 | 245 | withCleaner { clean => |
8eabb4ff MW |
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 | } |