Commit | Line | Data |
---|---|---|
8eabb4ff MW |
1 | /* -*-scala-*- |
2 | * | |
3 | * Miscellaneous utilities | |
4 | * | |
5 | * (c) 2018 Straylight/Edgeware | |
6 | */ | |
7 | ||
8 | /*----- Licensing notice --------------------------------------------------* | |
9 | * | |
10 | * This file is part of the Trivial IP Encryption (TrIPE) Android app. | |
11 | * | |
12 | * TrIPE is free software: you can redistribute it and/or modify it under | |
13 | * the terms of the GNU General Public License as published by the Free | |
14 | * Software Foundation; either version 3 of the License, or (at your | |
15 | * option) any later version. | |
16 | * | |
17 | * TrIPE is distributed in the hope that it will be useful, but WITHOUT | |
18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
19 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
20 | * for more details. | |
21 | * | |
22 | * You should have received a copy of the GNU General Public License | |
23 | * along with TrIPE. If not, see <https://www.gnu.org/licenses/>. | |
24 | */ | |
25 | ||
26 | package uk.org.distorted; package object tripe { | |
27 | ||
28 | /*----- Imports -----------------------------------------------------------*/ | |
29 | ||
0157de02 MW |
30 | import scala.language.{existentials, implicitConversions}; |
31 | ||
32 | import scala.collection.mutable.{HashSet, WeakHashMap}; | |
8eabb4ff | 33 | import scala.concurrent.duration.{Deadline, Duration}; |
c8292b34 | 34 | import scala.util.control.{Breaks, ControlThrowable}; |
8eabb4ff | 35 | |
c8292b34 | 36 | import java.io.{BufferedReader, Closeable, File, InputStream, Reader}; |
04a5abae | 37 | import java.net.{HttpURLConnection, URL, URLConnection}; |
8eabb4ff | 38 | import java.nio.{ByteBuffer, CharBuffer}; |
04a5abae MW |
39 | import java.nio.channels.{SelectionKey, Selector}; |
40 | import java.nio.channels.spi.{AbstractSelector, AbstractSelectableChannel}; | |
8eabb4ff | 41 | import java.nio.charset.Charset; |
a5ec891a | 42 | import java.text.SimpleDateFormat; |
04a5abae | 43 | import java.util.{Set => JSet}; |
8eabb4ff MW |
44 | import java.util.concurrent.locks.{Lock, ReentrantLock}; |
45 | ||
46 | /*----- Miscellaneous useful things ---------------------------------------*/ | |
47 | ||
48 | val rng = new java.security.SecureRandom; | |
49 | ||
50 | def unreachable(msg: String): Nothing = throw new AssertionError(msg); | |
c8292b34 MW |
51 | def unreachable(): Nothing = unreachable("unreachable"); |
52 | final val ok = (); | |
0157de02 MW |
53 | class Brand(val what: String) { |
54 | override def toString(): String = s"<${getClass.getName} $what>"; | |
55 | } | |
8eabb4ff MW |
56 | |
57 | /*----- Various pieces of implicit magic ----------------------------------*/ | |
58 | ||
59 | class InvalidCStringException(msg: String) extends Exception(msg); | |
8eabb4ff | 60 | |
25c35469 | 61 | object Implicits { |
8eabb4ff MW |
62 | |
63 | /* --- Syntactic sugar for locks --- */ | |
64 | ||
65 | implicit class LockOps(lk: Lock) { | |
66 | /* LK withLock { BODY } | |
67 | * LK.withLock(INTERRUPT) { BODY } | |
25c35469 MW |
68 | * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT } |
69 | * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT } | |
8eabb4ff MW |
70 | * |
71 | * Acquire a lock while executing a BODY. If a duration or deadline is | |
72 | * given then wait so long for the lock, and then give up and run ALT | |
73 | * instead. | |
74 | */ | |
75 | ||
76 | def withLock[T](dur: Duration, interrupt: Boolean) | |
77 | (body: => T): PendingLock[T] = | |
78 | new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero, | |
79 | interrupt, body); | |
80 | def withLock[T](dur: Duration)(body: => T): PendingLock[T] = | |
81 | withLock(dur, true)(body); | |
82 | def withLock[T](dl: Deadline, interrupt: Boolean) | |
83 | (body: => T): PendingLock[T] = | |
84 | new PendingLock(lk, dl.timeLeft, interrupt, body); | |
85 | def withLock[T](dl: Deadline)(body: => T): PendingLock[T] = | |
86 | withLock(dl, true)(body); | |
87 | def withLock[T](interrupt: Boolean)(body: => T): T = { | |
88 | if (interrupt) lk.lockInterruptibly(); | |
89 | else lk.lock(); | |
90 | try { body; } finally lk.unlock(); | |
91 | } | |
92 | def withLock[T](body: => T): T = withLock(true)(body); | |
93 | } | |
94 | ||
25c35469 | 95 | class PendingLock[T] private[Implicits] |
8eabb4ff MW |
96 | (val lk: Lock, val dur: Duration, |
97 | val interrupt: Boolean, body: => T) { | |
25c35469 | 98 | /* An auxiliary class for LockOps; provides the `orElse' qualifier. */ |
8eabb4ff | 99 | |
25c35469 | 100 | def orElse(alt: => T): T = { |
8eabb4ff MW |
101 | val locked = (dur, interrupt) match { |
102 | case (Duration.Inf, true) => lk.lockInterruptibly(); true | |
103 | case (Duration.Inf, false) => lk.lock(); true | |
104 | case (Duration.Zero, false) => lk.tryLock() | |
105 | case (_, true) => lk.tryLock(dur.length, dur.unit) | |
106 | case _ => unreachable("timed wait is always interruptible"); | |
107 | } | |
108 | if (!locked) alt; | |
109 | else try { body; } finally lk.unlock(); | |
110 | } | |
111 | } | |
0157de02 MW |
112 | |
113 | /* Implicit conversions to `Boolean'. I miss the way C integers and | |
114 | * pointers convert to boolean, so let's do that here. | |
115 | * | |
116 | * Numeric zero, null, and empty containers are all false; other objects | |
117 | * are true. | |
118 | */ | |
119 | implicit def truish(n: Byte): Boolean = n != 0; | |
120 | implicit def truish(n: Char): Boolean = n != 0; | |
121 | implicit def truish(n: Short): Boolean = n != 0; | |
122 | implicit def truish(n: Int): Boolean = n != 0; | |
123 | implicit def truish(n: Long): Boolean = n != 0; | |
124 | implicit def truish(n: Float): Boolean = n != 0; | |
125 | implicit def truish(n: Double): Boolean = n != 0; | |
126 | implicit def truish(x: AnyRef): Boolean = x != null; | |
127 | implicit def truish(s: String): Boolean = s != null && s != ""; | |
128 | implicit def truish(o: Option[_]): Boolean = o != None; | |
129 | implicit def truish(i: Iterator[_]): Boolean = i != null && i.hasNext; | |
130 | implicit def truish(c: Traversable[_]): Boolean = | |
131 | c != null && c.nonEmpty; | |
132 | ||
133 | /* Some additional bitwise operators. | |
134 | * | |
135 | * For now, just the `bic' operator `&~', because typing `& ~' is | |
136 | * inconsistent with my current style. | |
137 | */ | |
138 | class BitwiseIntImplicits(x: Int) { | |
139 | def &~(y: Byte): Int = x & ~y; | |
140 | def &~(y: Char): Int = x & ~y; | |
141 | def &~(y: Short): Int = x & ~y; | |
142 | def &~(y: Int): Int = x & ~y; | |
143 | def &~(y: Long): Long = x & ~y; | |
144 | } | |
145 | class BitwiseLongImplicits(x: Long) { | |
146 | def &~(y: Byte): Long = x & ~y; | |
147 | def &~(y: Char): Long = x & ~y; | |
148 | def &~(y: Short): Long = x & ~y; | |
149 | def &~(y: Int): Long = x & ~y; | |
150 | def &~(y: Long): Long = x & ~y; | |
151 | } | |
152 | implicit def bitwiseImplicits(x: Byte) = new BitwiseIntImplicits(x); | |
153 | implicit def bitwiseImplicits(x: Char) = new BitwiseIntImplicits(x); | |
154 | implicit def bitwiseImplicits(x: Short) = new BitwiseIntImplicits(x); | |
155 | implicit def bitwiseImplicits(x: Int) = new BitwiseIntImplicits(x); | |
156 | implicit def bitwiseImplicits(x: Long) = new BitwiseLongImplicits(x); | |
8eabb4ff MW |
157 | } |
158 | ||
0157de02 MW |
159 | import Implicits.truish; |
160 | ||
8eabb4ff MW |
161 | /*----- Cleanup assistant -------------------------------------------------*/ |
162 | ||
163 | class Cleaner { | |
164 | /* A helper class for avoiding deep nests of `try'/`finally'. | |
165 | * | |
166 | * Make a `Cleaner' instance CL at the start of your operation. Apply it | |
167 | * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate | |
168 | * cleanup actions. Finally, call CL.cleanup() to invoke the accumulated | |
169 | * actions, in reverse order. | |
170 | */ | |
171 | ||
172 | var cleanups: List[() => Unit] = Nil; | |
173 | def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } } | |
174 | def cleanup() { cleanups foreach { _() } } | |
175 | } | |
176 | ||
177 | def withCleaner[T](body: Cleaner => T): T = { | |
178 | /* An easier way to use the `Cleaner' class. Just | |
179 | * | |
180 | * withCleaner { CL => BODY } | |
181 | * | |
182 | * The BODY can attach cleanup actions to the cleaner CL by saying | |
183 | * CL { ACTION } as usual. When the BODY exits, normally or otherwise, the | |
184 | * cleanup actions are invoked in reverse order. | |
185 | */ | |
186 | ||
187 | val cleaner = new Cleaner; | |
188 | try { body(cleaner) } | |
189 | finally { cleaner.cleanup(); } | |
190 | } | |
191 | ||
192 | def closing[T, U <: Closeable](thing: U)(body: U => T): T = | |
193 | try { body(thing) } | |
194 | finally { thing.close(); } | |
195 | ||
c8292b34 MW |
196 | /*----- Control structures ------------------------------------------------*/ |
197 | ||
198 | private case class ExitBlock[T](brand: Brand, result: T) | |
199 | extends ControlThrowable; | |
200 | ||
201 | def block[T](body: (T => Nothing) => T): T = { | |
202 | /* block { exit[T] => ...; exit(x); ... } | |
203 | * | |
204 | * Execute the body until it calls the `exit' function or finishes. | |
205 | * Annoyingly, Scala isn't clever enough to infer the return type, so | |
206 | * you'll have to write it explicitly. | |
207 | */ | |
208 | ||
0157de02 | 209 | val mybrand = new Brand("block-exit"); |
c8292b34 MW |
210 | try { body { result => throw new ExitBlock(mybrand, result) } } |
211 | catch { | |
212 | case ExitBlock(brand, result) if brand eq mybrand => | |
213 | result.asInstanceOf[T] | |
214 | } | |
215 | } | |
216 | ||
217 | def blockUnit(body: (=> Nothing) => Unit) { | |
218 | /* blockUnit { exit => ...; exit; ... } | |
219 | * | |
220 | * Like `block'; it just saves you having to write `exit[Unit] => ...; | |
221 | * exit(ok); ...'. | |
222 | */ | |
223 | ||
0157de02 | 224 | val mybrand = new Brand("block-exit"); |
c8292b34 MW |
225 | try { body { throw new ExitBlock(mybrand, null) }; } |
226 | catch { case ExitBlock(brand, result) if brand eq mybrand => ok; } | |
227 | } | |
228 | ||
229 | def loop[T](body: (T => Nothing) => Unit): T = { | |
230 | /* loop { exit[T] => ...; exit(x); ... } | |
231 | * | |
232 | * Repeatedly execute the body until it calls the `exit' function. | |
233 | * Annoyingly, Scala isn't clever enough to infer the return type, so | |
234 | * you'll have to write it explicitly. | |
235 | */ | |
236 | ||
237 | block { exit => while (true) body(exit); unreachable } | |
238 | } | |
239 | ||
240 | def loopUnit(body: (=> Nothing) => Unit): Unit = { | |
241 | /* loopUnit { exit => ...; exit; ... } | |
242 | * | |
243 | * Like `loop'; it just saves you having to write `exit[Unit] => ...; | |
244 | * exit(()); ...'. | |
245 | */ | |
246 | ||
247 | blockUnit { exit => while (true) body(exit); } | |
248 | } | |
249 | ||
250 | val BREAKS = new Breaks; | |
251 | import BREAKS.{breakable, break}; | |
252 | ||
04a5abae MW |
253 | /*----- Interruptably doing things ----------------------------------------*/ |
254 | ||
255 | private class InterruptCatcher[T](body: => T, onWakeup: => Unit) | |
256 | extends AbstractSelector(null) { | |
257 | /* Hook onto the VM's thread interruption machinery. | |
258 | * | |
259 | * The `run' method is the only really interesting one. It will run the | |
260 | * BODY, returning its result; if the thread is interrupted during this | |
261 | * time, ONWAKEUP is invoked for effect. The expectation is that ONWAKEUP | |
262 | * will somehow cause BODY to stop early. | |
263 | * | |
264 | * Credit for this hack goes to Nicholas Wilson: see | |
265 | * <https://github.com/NWilson/javaInterruptHook>. | |
266 | */ | |
267 | ||
268 | private def nope: Nothing = | |
269 | { throw new UnsupportedOperationException("can't do that"); } | |
270 | protected def implCloseSelector() { } | |
271 | protected def register(chan: AbstractSelectableChannel, | |
272 | ops: Int, att: Any): SelectionKey = nope; | |
273 | def keys(): JSet[SelectionKey] = nope; | |
274 | def selectedKeys(): JSet[SelectionKey] = nope; | |
275 | def select(): Int = nope; | |
276 | def select(millis: Long): Int = nope; | |
277 | def selectNow(): Int = nope; | |
278 | ||
279 | def run(): T = try { | |
280 | begin(); | |
281 | val ret = body; | |
282 | if (Thread.interrupted()) throw new InterruptedException; | |
283 | ret | |
284 | } finally { | |
285 | end(); | |
286 | } | |
287 | def wakeup(): Selector = { onWakeup; this } | |
288 | } | |
289 | ||
290 | class PendingInterruptable[T] private[tripe](body: => T) { | |
291 | /* This class exists to provide the `onInterrupt THUNK' syntax. */ | |
292 | ||
293 | def onInterrupt(thunk: => Unit): T = | |
294 | new InterruptCatcher(body, thunk).run(); | |
295 | } | |
296 | def interruptably[T](body: => T) = { | |
297 | /* interruptably { BODY } onInterrupt { THUNK } | |
298 | * | |
299 | * Execute BODY and return its result. If the thread receives an | |
300 | * interrupt -- or is already in an interrupted state -- execute THUNK for | |
301 | * effect; it is expected to cause BODY to return expeditiously, and when | |
302 | * the BODY completes, an `InterruptedException' is thrown. | |
303 | */ | |
304 | ||
305 | new PendingInterruptable(body); | |
306 | } | |
307 | ||
8eabb4ff MW |
308 | /*----- A gadget for fetching URLs ----------------------------------------*/ |
309 | ||
310 | class URLFetchException(msg: String) extends Exception(msg); | |
311 | ||
312 | trait URLFetchCallbacks { | |
313 | def preflight(conn: URLConnection) { } | |
c8292b34 | 314 | def write(buf: Array[Byte], n: Int, len: Long): Unit; |
8eabb4ff MW |
315 | def done(win: Boolean) { } |
316 | } | |
317 | ||
318 | def fetchURL(url: URL, cb: URLFetchCallbacks) { | |
319 | /* Fetch the URL, feeding the data through the callbacks CB. */ | |
320 | ||
321 | withCleaner { clean => | |
04a5abae | 322 | var win: Boolean = false; clean { cb.done(win); } |
8eabb4ff | 323 | |
04a5abae MW |
324 | /* Set up the connection. This isn't going to block, I think, and we |
325 | * need to use it in the interrupt handler. | |
326 | */ | |
8eabb4ff | 327 | val c = url.openConnection(); |
8eabb4ff | 328 | |
04a5abae MW |
329 | /* Java's default URL handlers don't respond to interrupts, so we have to |
330 | * take over this duty. | |
8eabb4ff | 331 | */ |
04a5abae MW |
332 | interruptably { |
333 | /* Run the caller's preflight check. This must be done here, since it | |
334 | * might well block while it discovers things like the content length. | |
335 | */ | |
336 | cb.preflight(c); | |
337 | ||
338 | /* Start fetching data. */ | |
339 | val in = c.getInputStream; clean { in.close(); } | |
340 | val explen = c.getContentLength; | |
341 | ||
342 | /* Read a buffer at a time, and give it to the callback. Maintain a | |
343 | * running total. | |
344 | */ | |
345 | var len: Long = 0; | |
346 | blockUnit { exit => | |
347 | for ((buf, n) <- blocks(in)) { | |
348 | cb.write(buf, n, len); | |
349 | len += n; | |
350 | if (explen != -1 && len > explen) exit; | |
351 | } | |
c8292b34 | 352 | } |
8eabb4ff | 353 | |
04a5abae MW |
354 | /* I can't find it documented anywhere that the existing machinery |
355 | * checks the received stream against the advertised content length. | |
356 | * It doesn't hurt to check again, anyway. | |
357 | */ | |
358 | if (explen != -1 && explen != len) { | |
359 | throw new URLFetchException( | |
360 | s"received $len /= $explen bytes from `$url'"); | |
361 | } | |
8eabb4ff | 362 | |
04a5abae MW |
363 | /* Glorious success is ours. */ |
364 | win = true; | |
365 | } onInterrupt { | |
366 | /* Oh. How do we do this? */ | |
367 | ||
368 | c match { | |
369 | case c: HttpURLConnection => | |
370 | /* It's an HTTP connection (what happened to the case here?). | |
371 | * HTTPS connections match too because they're a subclass. Getting | |
372 | * the input stream will block, but there's an easier way. | |
373 | */ | |
374 | c.disconnect(); | |
375 | ||
376 | case _ => | |
377 | /* It's something else. Let's hope that getting the input stream | |
378 | * doesn't block. | |
379 | */ | |
380 | c.getInputStream.close(); | |
381 | } | |
382 | } | |
8eabb4ff MW |
383 | } |
384 | } | |
385 | ||
8eabb4ff MW |
386 | /*----- Threading things --------------------------------------------------*/ |
387 | ||
04a5abae MW |
388 | def thread(name: String, run: Boolean = true, daemon: Boolean = true) |
389 | (f: => Unit): Thread = { | |
8eabb4ff MW |
390 | /* Make a thread with a given name, and maybe start running it. */ |
391 | ||
392 | val t = new Thread(new Runnable { def run() { f; } }, name); | |
393 | if (daemon) t.setDaemon(true); | |
394 | if (run) t.start(); | |
395 | t | |
396 | } | |
397 | ||
04a5abae MW |
398 | class ValueThread[T](name: String, group: ThreadGroup = null, |
399 | stacksz: Long = 0)(body: => T) | |
400 | extends Thread(group, null, name, stacksz) { | |
401 | private[this] var exc: Throwable = _; | |
402 | private[this] var ret: T = _; | |
403 | ||
404 | override def run() { | |
405 | try { ret = body; } | |
406 | catch { case e: Throwable => exc = e; } | |
407 | } | |
408 | def get: T = | |
409 | if (isAlive) throw new IllegalArgumentException("still running"); | |
410 | else if (exc != null) throw exc; | |
411 | else ret; | |
412 | } | |
413 | def valueThread[T](name: String, run: Boolean = true) | |
414 | (body: => T): ValueThread[T] = { | |
415 | val t = new ValueThread(name)(body); | |
416 | if (run) t.start(); | |
417 | t | |
418 | } | |
419 | ||
8eabb4ff MW |
420 | /*----- Quoting and parsing tokens ----------------------------------------*/ |
421 | ||
422 | def quoteTokens(v: Seq[String]): String = { | |
423 | /* Return a string representing the token sequence V. | |
424 | * | |
425 | * The tokens are quoted as necessary. | |
426 | */ | |
427 | ||
428 | val b = new StringBuilder; | |
429 | var sep = false; | |
430 | for (s <- v) { | |
431 | ||
432 | /* If this isn't the first word, then write a separating space. */ | |
433 | if (!sep) sep = true; | |
434 | else b += ' '; | |
435 | ||
436 | /* Decide how to handle this token. */ | |
437 | if (s.length > 0 && | |
438 | (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' && | |
439 | !ch.isWhitespace) })) { | |
440 | /* If this word is nonempty and contains no problematic characters, | |
441 | * we can write it literally. | |
442 | */ | |
443 | ||
444 | b ++= s; | |
445 | } else { | |
446 | /* Otherwise, we shall have to do this the hard way. We could be | |
447 | * cleverer about this, but it's not worth the effort. | |
448 | */ | |
449 | ||
450 | b += '"'; | |
451 | s foreach { ch => | |
452 | if (ch == '"' || ch == '\\') b += '\\'; | |
453 | b += ch; | |
454 | } | |
455 | b += '"'; | |
456 | } | |
457 | } | |
458 | b.result | |
459 | } | |
460 | ||
461 | class InvalidQuotingException(msg: String) extends Exception(msg); | |
462 | ||
463 | def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = { | |
464 | /* Parse the next token from a string S. | |
465 | * | |
466 | * If there is a token in S starting at or after index POS, then return | |
467 | * it, and the index for the following token; otherwise return `None'. | |
468 | */ | |
469 | ||
470 | val b = new StringBuilder; | |
471 | val n = s.length; | |
472 | var i = pos; | |
473 | var q = 0; | |
474 | ||
475 | /* Skip whitespace while we find the next token. */ | |
476 | while (i < n && s(i).isWhitespace) i += 1; | |
477 | ||
478 | /* Maybe there just isn't anything to find. */ | |
479 | if (i >= n) return None; | |
480 | ||
481 | /* There is something there. Unpick the quoting and escaping. */ | |
0157de02 | 482 | while (i < n && (q || !s(i).isWhitespace)) { |
8eabb4ff MW |
483 | s(i) match { |
484 | case '\\' => | |
485 | if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'"); | |
486 | b += s(i + 1); i += 2; | |
487 | case ch@('"' | ''') => | |
0157de02 | 488 | if (!q) q = ch; |
8eabb4ff MW |
489 | else if (q == ch) q = 0; |
490 | else b += ch; | |
491 | i += 1; | |
492 | case ch => | |
493 | b += ch; | |
494 | i += 1; | |
495 | } | |
496 | } | |
497 | ||
498 | /* Check that the quoting was valid. */ | |
0157de02 | 499 | if (q) throw new InvalidQuotingException(s"unmatched `$q'"); |
8eabb4ff MW |
500 | |
501 | /* Skip whitespace before the next token. */ | |
502 | while (i < n && s(i).isWhitespace) i += 1; | |
503 | ||
504 | /* We're done. */ | |
505 | Some((b.result, i)) | |
506 | } | |
507 | ||
508 | def splitTokens(s: String, pos: Int = 0): Seq[String] = { | |
509 | /* Return all of the tokens in string S into tokens, starting at POS. */ | |
510 | ||
511 | val b = List.newBuilder[String]; | |
512 | var i = pos; | |
513 | ||
c8292b34 MW |
514 | loopUnit { exit => nextToken(s, i) match { |
515 | case Some((w, j)) => b += w; i = j; | |
516 | case None => exit; | |
517 | } } | |
8eabb4ff MW |
518 | b.result |
519 | } | |
520 | ||
0157de02 MW |
521 | /*----- Hooks -------------------------------------------------------------*/ |
522 | ||
523 | /* This is a really simple publisher/subscriber system. The only slight | |
524 | * tweak -- and the reason I'm not just using the Scala machinery -- is that | |
525 | * being attached to a hook doesn't prevent the client from being garbage | |
526 | * collected. | |
527 | */ | |
528 | ||
529 | trait BaseHookClient[E] { | |
530 | /* The minimal requirements for a hook client. Honestly you should be | |
531 | * using `HookClient' instead. | |
532 | */ | |
533 | ||
534 | type H = Hook[E]; // the type of hook we attach to | |
535 | def hook(hk: H, evt: E); // called with events from the hook | |
536 | } | |
537 | ||
538 | trait HookClient[E] extends BaseHookClient[E] { | |
539 | /* The properly cooked hook client. This keeps track of which hooks we're | |
540 | * attached to so we can release them all easily. | |
541 | */ | |
542 | ||
543 | private val hooks = new HashSet[H]; | |
544 | protected def attachHook(hk: H) { hk.addHookClient(this); hooks += hk; } | |
545 | protected def detachHook(hk: H) { hk.rmHookClient(this); hooks -= hk; } | |
546 | protected def detachAllHooks() | |
547 | { for (hk <- hooks) hk.rmHookClient(this); hooks.clear(); } | |
548 | } | |
549 | ||
550 | trait Hook[E] { | |
551 | type C = BaseHookClient[E]; | |
552 | private val clients = new WeakHashMap[C, Unit]; | |
553 | def addHookClient(c: C) { clients(c) = (); } | |
554 | def rmHookClient(c: C) { clients -= c; } | |
555 | protected def callHook(evt: E) | |
556 | { for (c <- clients.keys) c.hook(this, evt); } | |
557 | } | |
558 | ||
559 | /*----- Fluid variables ---------------------------------------------------*/ | |
560 | ||
561 | object BaseFluid { | |
562 | /* The multi-fluid `let' form is defined here so that it can access the | |
563 | * `capture' method of individual fluids, but users should use the | |
564 | * package-level veneer. | |
565 | */ | |
566 | ||
567 | private[tripe] def let[U](fxs: (BaseFluid[T], T) forSome { type T }*) | |
568 | (body: => U): U = { | |
569 | /* See the package-level `let' for details. */ | |
570 | val binds = for ((f, _) <- fxs) yield f.capture; | |
571 | try { for ((f, x) <- fxs) f.v = x; body } | |
572 | finally { for (b <- binds) b.restore(); } | |
573 | } | |
574 | } | |
575 | def let[U](fxs: (BaseFluid[T], T) forSome { type T }*)(body: => U): U = { | |
576 | /* let(F -> X, ...) { BODY } | |
577 | * | |
578 | * Evaluate BODY in a dynamic context where each fluid F is bound to the | |
579 | * corresponding value X. | |
580 | */ | |
581 | ||
582 | BaseFluid.let(fxs: _*)(body); | |
583 | } | |
584 | ||
585 | trait BaseFluid[T] { | |
586 | /* The basic fluid protocol. */ | |
587 | ||
588 | override def toString(): String = | |
589 | f"${getClass.getName}%s@${hashCode}%x($v%s)"; | |
590 | ||
591 | protected trait Binding { | |
592 | /* A captured binding which can be restored later. Implementing this is | |
593 | * a subclass responsibility. | |
594 | */ | |
595 | ||
596 | def restore(); | |
597 | /* Restore the fluid's state to the state captured here. */ | |
598 | } | |
599 | ||
600 | /* Fetch and modify the current binding. */ | |
601 | def v: T; | |
602 | def v_=(x: T); | |
603 | ||
604 | protected def capture: Binding; | |
605 | /* Capture and the current state of the fluid. */ | |
606 | ||
607 | def let[U](x: T)(body: => U): U = { | |
608 | /* let(X) { BODY } | |
609 | * | |
610 | * Evaluate BODY in a dynamic context where the fluid is bound to the | |
611 | * value X. | |
612 | */ | |
613 | ||
614 | val b = capture; | |
615 | try { v = x; body } finally { b.restore(); } | |
616 | } | |
617 | } | |
618 | ||
619 | class SharedFluid[T](init: T) extends BaseFluid[T] { | |
620 | /* A simple global fluid. It's probably a mistake to try to access a | |
621 | * `SharedFluid' from multiple threads without serious synchronization. | |
622 | */ | |
623 | ||
624 | var v: T = init; | |
625 | private class Binding(old: T) extends super.Binding | |
626 | { def restore() { v = old; } } | |
627 | protected def capture: super.Binding = new Binding(v); | |
628 | } | |
629 | ||
630 | class ThreadFluid[T](init: T) extends BaseFluid[T] { | |
631 | /* A thread-aware fluid. The top-level binding is truly global, shared by | |
632 | * all threads, but `let'-bindings are thread-local. | |
633 | */ | |
634 | ||
635 | private[this] var global: T = init; | |
636 | private[this] var bound: ThreadLocal[Option[T]] = new ThreadLocal; | |
637 | bound.set(None); | |
638 | ||
639 | def v: T = bound.get match { case None => global; case Some(x) => x; }; | |
640 | def v_=(x: T) { bound.get match { | |
641 | case None => global = x; | |
642 | case _ => bound.set(Some(x)); | |
643 | } } | |
644 | ||
645 | private class Binding(old: Option[T]) extends super.Binding | |
646 | { def restore() { bound.set(old); } } | |
647 | protected def capture: super.Binding = new Binding(bound.get); | |
648 | } | |
649 | ||
c8292b34 MW |
650 | /*----- Other random things -----------------------------------------------*/ |
651 | ||
8eabb4ff | 652 | trait LookaheadIterator[T] extends BufferedIterator[T] { |
c8292b34 MW |
653 | /* An iterator in terms of a single `maybe there's another item' function. |
654 | * | |
655 | * It seems like every time I write an iterator in Scala, the only way to | |
656 | * find out whether there's a next item, for `hasNext', is to actually try | |
657 | * to fetch it. So here's an iterator in terms of a function which goes | |
658 | * off and maybe returns a next thing. It turns out to be easy to satisfy | |
659 | * the additional requirements for `BufferedIterator', so why not? | |
660 | */ | |
661 | ||
662 | /* Subclass responsibility. */ | |
8eabb4ff | 663 | protected def fetch(): Option[T]; |
c8292b34 MW |
664 | |
665 | /* The machinery. `st' is `None' if there's no current item, null if we've | |
666 | * actually hit the end, or `Some(x)' if the current item is x. | |
667 | */ | |
668 | private[this] var st: Option[T] = None; | |
8eabb4ff | 669 | private[this] def peek() { |
c8292b34 | 670 | /* Arrange to have a current item. */ |
8eabb4ff MW |
671 | if (st == None) fetch() match { |
672 | case None => st = null; | |
673 | case x@Some(_) => st = x; | |
674 | } | |
675 | } | |
c8292b34 MW |
676 | |
677 | /* The `BufferedIterator' protocol. */ | |
8eabb4ff | 678 | override def hasNext: Boolean = { peek(); st != null } |
c8292b34 | 679 | override def head: T = |
8eabb4ff | 680 | { peek(); if (st == null) throw new NoSuchElementException; st.get } |
c8292b34 | 681 | override def next(): T = { val it = head; st = None; it } |
8eabb4ff MW |
682 | } |
683 | ||
c8292b34 MW |
684 | def bufferedReader(r: Reader): BufferedReader = r match { |
685 | case br: BufferedReader => br | |
686 | case _ => new BufferedReader(r) | |
687 | } | |
8eabb4ff | 688 | |
c8292b34 MW |
689 | def lines(r: BufferedReader): BufferedIterator[String] = |
690 | new LookaheadIterator[String] { | |
691 | /* Iterates over the lines of text in a `Reader' object. */ | |
692 | override protected def fetch() = Option(r.readLine()); | |
693 | } | |
694 | def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r)); | |
695 | ||
696 | def blocks(in: InputStream, blksz: Int): | |
697 | BufferedIterator[(Array[Byte], Int)] = | |
698 | /* Iterates over (possibly irregularly sized) blocks in a stream. */ | |
699 | new LookaheadIterator[(Array[Byte], Int)] { | |
700 | val buf = new Array[Byte](blksz) | |
701 | override protected def fetch() = { | |
702 | val n = in.read(buf); | |
703 | if (n < 0) None | |
704 | else Some((buf, n)) | |
705 | } | |
706 | } | |
707 | def blocks(in: InputStream): | |
04a5abae | 708 | BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536); |
c8292b34 MW |
709 | |
710 | def blocks(in: BufferedReader, blksz: Int): | |
711 | BufferedIterator[(Array[Char], Int)] = | |
712 | /* Iterates over (possibly irregularly sized) blocks in a reader. */ | |
713 | new LookaheadIterator[(Array[Char], Int)] { | |
714 | val buf = new Array[Char](blksz) | |
715 | override protected def fetch() = { | |
716 | val n = in.read(buf); | |
717 | if (n < 0) None | |
718 | else Some((buf, n)) | |
719 | } | |
8eabb4ff | 720 | } |
c8292b34 | 721 | def blocks(in: BufferedReader): |
04a5abae | 722 | BufferedIterator[(Array[Char], Int)] = blocks(in, 65536); |
c8292b34 MW |
723 | def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] = |
724 | blocks(bufferedReader(r), blksz); | |
725 | def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] = | |
726 | blocks(bufferedReader(r)); | |
727 | ||
728 | def oxford(conj: String, things: Seq[String]): String = things match { | |
729 | case Seq() => "<nothing>" | |
730 | case Seq(a) => a | |
731 | case Seq(a, b) => s"$a $conj $b" | |
732 | case Seq(a, tail@_*) => | |
733 | val sb = new StringBuilder; | |
734 | sb ++= a; sb ++= ", "; | |
735 | def iter(rest: Seq[String]) { | |
736 | rest match { | |
737 | case Seq() => unreachable; | |
738 | case Seq(a) => sb ++= conj; sb += ' '; sb ++= a; | |
739 | case Seq(a, tail@_*) => sb ++= a; sb ++= ", "; iter(tail); | |
740 | } | |
741 | } | |
742 | iter(tail); | |
743 | sb.result | |
8eabb4ff MW |
744 | } |
745 | ||
a5ec891a MW |
746 | val datefmt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); |
747 | ||
748 | def formatDuration(t: Int): String = | |
04a5abae MW |
749 | if (t < -1) "???" |
750 | else { | |
751 | val (s, t1) = (t%60, t/60); | |
752 | val (m, h) = (t1%60, t1/60); | |
753 | if (h > 0) f"$h%d:$m%02d:$s%02d" | |
754 | else f"$m%02d:$s%02d" | |
755 | } | |
756 | ||
8eabb4ff MW |
757 | /*----- That's all, folks -------------------------------------------------*/ |
758 | ||
759 | } |