X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe-android/blobdiff_plain/8eabb4ff13562f3550499ee599297f7e97fa8754..HEAD:/sys.scala diff --git a/sys.scala b/sys.scala index a6c00c3..43487b5 100644 --- a/sys.scala +++ b/sys.scala @@ -1,6 +1,6 @@ /* -*-scala-*- * - * System calls and errors + * System-level hacking * * (c) 2018 Straylight/Edgeware */ @@ -27,35 +27,182 @@ package uk.org.distorted.tripe; package object sys { /*----- Imports -----------------------------------------------------------*/ -import scala.collection.mutable.HashSet; +import scala.collection.convert.decorateAsJava._; +import scala.collection.mutable.{HashMap, HashSet}; -import java.io.File; +import java.io.{BufferedReader, BufferedWriter, Closeable, File, + FileDescriptor, FileInputStream, FileOutputStream, + FileReader, FileWriter, + InputStream, InputStreamReader, + OutputStream, OutputStreamWriter}; +import java.nio.{ByteBuffer, CharBuffer}; +import java.nio.charset.Charset; +import java.util.Date; -import Magic._; +import Implicits.truish; + +/*----- Some magic for C strings ------------------------------------------*/ + +type CString = Array[Byte]; + +/* We do this by hand, rather than relying on the JNI's built-in conversions, + * because we use the default encoding taken from the locale settings, rather + * than the ridiculous `modified UTF-8' which is (a) insensitive to the + * user's chosen locale and (b) not actually UTF-8 either. + */ + +class InvalidCStringException(msg: String) extends Exception(msg); + +object StringImplicits { + implicit class ConvertJStringToCString(s: String) { + /* Magic to convert a string into a C string (null-terminated bytes). */ + + def toCString: CString = { + /* Convert the receiver to a C string. */ + + val enc = Charset.defaultCharset.newEncoder; + val in = CharBuffer.wrap(s); + var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt; + var out = ByteBuffer.allocate(sz); + + loop[CString] { exit => + /* If there's still stuff to encode, then encode it. Otherwise, + * there must be some dregs left in the encoder, so flush them out. + */ + val r = if (in.hasRemaining) enc.encode(in, out, true) + else enc.flush(out); + + /* Sift through the wreckage to figure out what to do. */ + if (r.isError) r.throwException(); + else if (r.isOverflow) { + /* No space in the buffer. Make it bigger. */ + + sz *= 2; + val newout = ByteBuffer.allocate(sz); + out.flip(); newout.put(out); + out = newout; + } else if (r.isUnderflow) { + /* All done. Check that there are no unexpected zero bytes -- so + * this will indeed be a valid C string -- and convert into a byte + * array that the C code will be able to pick apart. + */ + + out.flip(); val n = out.limit; val u = out.array; + if ({val z = u.indexOf(0); 0 <= z && z < n}) + throw new InvalidCStringException("null byte in encoding"); + val v = new Array[Byte](n + 1); + out.array.copyToArray(v, 0, n); + v(n) = 0; + exit(v); + } + } + } + } + + implicit class ConvertCStringToJString(v: CString) { + /* Magic to convert a C string into a `proper' string. */ + + def toJString: String = { + /* Convert the receiver to a C string. + * + * We do this by hand, rather than relying on the JNI's built-in + * conversions, because we use the default encoding taken from the + * locale settings, rather than the ridiculous `modified UTF-8' which + * is (a) insensitive to the user's chosen locale and (b) not actually + * UTF-8 either. + */ + + val inlen = v.indexOf(0) match { + case -1 => v.length + case n => n + } + val dec = Charset.defaultCharset.newDecoder; + val in = ByteBuffer.wrap(v, 0, inlen); + dec.decode(in).toString + } + } +} +import StringImplicits._; + +/*----- Main code ---------------------------------------------------------*/ + +/* Import the native code library. */ +System.loadLibrary("tripe"); + +/* Native types. + * + * See `jni.c'. There's no good way to hand a C pointer into Java, so we + * just copy whole structures into Java byte arrays and hope. Well, also we + * tag them so we can detect mixups. + */ +type Wrapper = Array[Byte]; +class NativeObjectTypeException(msg: String) extends RuntimeException(msg); /*----- Error codes -------------------------------------------------------*/ +/* Machinery for collecting error information from C. */ +protected case class ErrorEntry(val tag: String, val err: Int); +@native protected def errtab: Array[ErrorEntry]; +@native protected def strerror(err: Int): CString; + object Errno extends Enumeration { - private[this] val tagmap = { + /* System errors. + * + * There are two slight difficulties here. + * + * * Not all target systems have the same errors. C has a preprocessor + * to deal with this, but we don't; so instead we'll define all of the + * errors we'll ever need, but maybe with bogus values. + * + * * Some systems reuse code numbers for two different error names, e.g., + * both `EAGAIN' and `EWOULDBLOCK' are the same on Linux -- but not + * necessarily on other systems. Scala's `Enumeration' machinery + * doesn't like sharing `id' numbers between values. + * + * We augment the value type with an additional `code' value which is the + * actual system error code; we arbitrarily pick one error symbol with a + * given code to be `canonical', i.e., it has E.id == E.code; the others + * have synthetic `id' values. And symbols which don't correspond to any + * error on the target system have synthetic `id' /and/ `code', so that + * they can still be spoken about, but won't match any real error. + */ + + private val tagmap = { // map names to numbers based on what C reports val b = Map.newBuilder[String, Int]; - for (jni.ErrorEntry(tag, err) <- jni.errtab) b += tag -> err; + for (ErrorEntry(tag, err) <- errtab) b += tag -> err; b.result } - private[this] var wrong = -255; - private[this] val seen = HashSet[Int](); - class ErrnoVal private[Errno](tag: String, val code: Int, id: Int) - extends Val(id, tag) { - def message: String = jni.strerror(code).toJString; + private val seen = HashSet[Int](); // which error codes have been taken + + private var wrong = -256; // next synthetic code + private def nextwrong: Int = { val w = wrong; wrong -= 1; w } + + class Val private[Errno](tag: String, val code: Int, id: Int) + extends super.Val(id, tag) { + /* Our augmented error type. */ + + def message: String = strerror(code).toJString; + } + private class UnknownError(code: Int) + extends Val("", code, code); + + private def err(tag: String, code: Int): Val = { + /* Construct an error symbol given its tag string and a code number. */ + + if (code < 0) new Val(tag, code, code) + else if (seen contains code) new Val(tag, code, nextwrong) + else { seen += code; new Val(tag, code, code) } } + private def err(tag: String): Val = + err(tag, tagmap.getOrElse(tag, nextwrong)); - private[this] def err(tag: String, code: Int): ErrnoVal = { - if (seen contains code) { wrong -= 1; new ErrnoVal(tag, code, wrong) } - else { seen += code; new ErrnoVal(tag, code, code) } + def byid(id: Int): Value = { + if (seen contains id) apply(id) + else new UnknownError(id) } - private[this] def err(tag: String): ErrnoVal = err(tag, tagmap(tag)); - val OK = err("OK", 0); + val OK = err("OK", 0); // `errno' zero is a real thing /* ;;; The errno name table is very boring to type. To make life less @@ -233,27 +380,337 @@ object Errno extends Enumeration { val EHWPOISON = err("EHWPOISON"); /***end***/ } -import Errno.{Value => _, _}; +import Errno.{Val => Errno, EEXIST, EISDIR, ENOENT, ENOTDIR}; object SystemError { - def apply(err: Errno.Value, what: String): SystemError = + /* Pattern matching for `SystemError', below. */ + + def apply(err: Errno, what: String): SystemError = new SystemError(err, what); - def unapply(e: Exception): Option[(Errno.Value, String)] = e match { + def unapply(e: Exception): Option[(Errno, String)] = e match { case e: SystemError => Some((e.err, e.what)) case _ => None } } +class SystemError (val err: Errno, val what: String) extends Exception { + /* An error from a syscall or similar, usually from native code. */ + + /* A constructor which takes an error number, for easier access from C. */ + private def this(err: Int, what: CString) + { this(Errno.byid(err).asInstanceOf[Errno], what.toJString); } -class SystemError private[this](val err: Errno.ErrnoVal, val what: String) - extends Exception { - def this(err: Errno.Value, what: String) - { this(err.asInstanceOf[Errno.ErrnoVal], what); } - private[tripe] def this(err: Int, what: CString) - { this(Errno(err), what.toJString); } override def getMessage(): String = s"$what: ${err.message}"; } -/*----- Filesystem hacks --------------------------------------------------*/ +/*----- Basic file operations ---------------------------------------------*/ + +@native protected def unlink(path: CString); +def unlink(path: String) { unlink(path.toCString); } +@native protected def rmdir(path: CString); +def rmdir(path: String) { rmdir(path.toCString); } +@native protected def mkdir(path: CString, mode: Int); +def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); } +@native protected def chmod(path: CString, mode: Int); +def chmod(path: String, mode: Int) { chmod(path.toCString, mode); } +@native protected def mkfile(path: CString, mode: Int); +def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); } +@native protected def rename(from: CString, to: CString); +def rename(from: String, to: String) + { rename(from.toCString, to.toCString); } + +@native def fdint(fd: FileDescriptor): Int; +@native def newfd(fd: Int): FileDescriptor; +@native def isatty(fd: FileDescriptor): Boolean; + +/*----- File status information -------------------------------------------*/ + +/* These are the traditional values, but the C code carefully arranges to + * return them regardless of what your kernel actually thinks. + */ +final val S_IFMT = 0xf000; +final val S_IFIFO = 0x1000; +final val S_IFCHR = 0x2000; +final val S_IFDIR = 0x4000; +final val S_IFBLK = 0x6000; +final val S_IFREG = 0x8000; +final val S_IFLNK = 0xa000; +final val S_IFSOCK = 0xc000; + +/* Primitive read-the-file-status calls. */ +@native protected def stat(path: CString): sys.FileInfo; +def stat(path: String): sys.FileInfo = stat(path.toCString); +@native protected def lstat(path: CString): sys.FileInfo; +def lstat(path: String): sys.FileInfo = lstat(path.toCString); + +object FileInfo extends Enumeration { + /* A simple enumeration of things a file might be. + * + * `HDLNK' is a hard link, used in `tar' files. + */ + val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, HDLNK, UNK = Value; + type Type = Value; +} +import FileInfo._; + +class FileInfo private[this](val devMajor: Int, val devMinor: Int, + val ino: Long, val mode: Int, val nlink: Int, + val uid: Int, val gid: Int, + _rdevMinor: Int, _rdevMajor: Int, + val size: Long, + val blksize: Int, val blocks: Long, + val atime: Date, val mtime: Date, + val ctime: Date) { + /* Information about a file. This is constructed directly from native + * code. + */ + + private def this(devMajor: Int, devMinor: Int, ino: Long, + mode: Int, nlink: Int, uid: Int, gid: Int, + rdevMinor: Int, rdevMajor: Int, + size: Long, blksize: Int, blocks: Long, + atime: Long, mtime: Long, ctime: Long) { + /* Lightly cook the values from the underlying `struct stat'. */ + + this(devMajor, devMinor, ino, mode, nlink, uid, gid, + rdevMajor, rdevMinor, size, blksize, blocks, + new Date(atime), new Date(mtime), new Date(ctime)); + } + + /* Return the file permissions only. */ + def perms: Int = mode&0xfff; + + /* Return the filetype, as a `FileInfo.Type'. */ + def ftype: Type = (mode&S_IFMT) match { + case S_IFIFO => FIFO + case S_IFCHR => CHR + case S_IFDIR => DIR + case S_IFBLK => BLK + case S_IFREG => REG + case S_IFLNK => LNK + case S_IFSOCK => SOCK + case _ => UNK + } + + private[this] def mustBeDevice() { + /* Insist that you only ask for `rdev' fields on actual device nodes. */ + ftype match { + case CHR | BLK => ok; + case _ => throw new IllegalArgumentException("Object is not a device"); + } + } + + /* Query the device-node numbers. */ + def rdevMajor: Int = { mustBeDevice(); _rdevMajor } + def rdevMinor: Int = { mustBeDevice(); _rdevMinor } +} + +/*----- Listing directories -----------------------------------------------*/ + +/* Primitive operations. */ +@native protected def opendir(path: CString): Wrapper; +@native protected def readdir(path: CString, dir: Wrapper): CString; +@native protected def closedir(path: CString, dir: Wrapper); + +protected abstract class BaseDirIterator[T](cpath: CString) + extends LookaheadIterator[T] with Closeable { + /* The underlying machinery for directory iterators. + * + * Subclasses must define `mangle' to convert raw filenames into a T. + * We keep track of the path C-string, because we need to keep passing that + * back to C for inclusion in error messages. Recording higher-level + * things is left for subclasses. + */ + + /* Constructors from more convenient types. */ + def this(path: String) { this(path.toCString); } + def this(dir: File) { this(dir.getPath); } + + /* Cleaning up after ourselves. */ + override def close() { closedir(cpath, dir); } + override protected def finalize() { super.finalize(); close(); } + + /* Subclass responsibility. */ + protected def mangle(file: String): T; + + /* Main machinery. */ + private[this] val dir = opendir(cpath); + override protected def fetch(): Option[T] = readdir(cpath, dir) match { + case null => None + case f => f.toJString match { + case "." | ".." => fetch() + case jf => Some(mangle(jf)) + } + } +} + +class DirIterator(val path: String) extends BaseDirIterator[String](path) { + /* Iterator over the basenames of files in a directory. */ + + def this(dir: File) { this(dir.getPath); } + + override protected def mangle(file: String): String = file; +} + +class DirFilesIterator private[this](val dir: File, cpath: CString) + extends BaseDirIterator[File](cpath) { + /* Iterator over full `File' objects in a directory. */ + + def this(dir: File) { this(dir, dir.getPath.toCString); } + def this(path: String) { this(new File(path), path.toCString); } + + override protected def mangle(file: String): File = new File(dir, file); +} + +/*----- File locking ------------------------------------------------------*/ + +/* Primitive operations. The low `mode' bits are for the lock file if we + * have to create it. + */ +final val LKF_EXCL = 0x1000; +final val LKF_WAIT = 0x2000; +@native protected def lock(path: CString, mode: Int): Wrapper; +@native protected def unlock(lock: Wrapper); + +class FileLock(path: String, flags: Int) extends Closeable { + /* A class which represents a held lock on a file. */ + + /* Constructors. The default is to take an exclusive lock or fail + * immediately. + */ + def this(file: File, flags: Int) { this(file.getPath, flags); } + def this(path: String) { this(path, LKF_EXCL | 0x1b6); } + def this(file: File) { this(file.getPath, LKF_EXCL | 0x1b6); } + + /* The low-level lock object, actually a file descriptor. */ + private[this] val lk = lock(path.toCString, flags); + + /* Making sure things get cleaned up. */ + override def close() { unlock(lk); } + override protected def finalize() { super.finalize(); close(); } +} + +/*----- File implicits ----------------------------------------------------*/ + +object FileImplicits { + implicit class FileOps(file: File) { + /* Augment `File' with operations which throw informative (if low-level + * and system-specific) exceptions rather than returning unhelpful + * win/lose booleans. These have names ending with `_!' because they + * might explode. + * + * And some other useful methods. + */ + + /* Constructing names of files in a directory. Honestly, I'm surprised + * there isn't a method for this already. + */ + def /(sub: String): File = new File(file, sub); + + /* Simple file operations. */ + def unlink_!() { unlink(file.getPath); } + def rmdir_!() { rmdir(file.getPath); } + def mkdir_!(mode: Int) { mkdir(file.getPath, mode); } + def mkdir_!() { mkdir_!(0x1ff); } + def chmod_!(mode: Int) { chmod(file.getPath, mode); } + def mkfile_!(mode: Int) { mkfile(file.getPath, mode); } + def mkfile_!() { mkfile_!(0x1b6); } + def rename_!(to: File) { rename(file.getPath, to.getPath); } + + /* Listing directories. */ + def withFilesIterator[T](body: DirFilesIterator => T): T = { + val iter = new DirFilesIterator(file.getPath); + try { body(iter) } finally { iter.close(); } + } + def foreachFile(fn: File => Unit) { withFilesIterator(_.foreach(fn)) } + def files_! : Seq[File] = withFilesIterator { _.toSeq }; + + /* Low-level lFile information. */ + def stat_! : FileInfo = stat(file.getPath); + def lstat_! : FileInfo = lstat(file.getPath); + + /* Specific file-status queries. */ + private[this] def statish[T](statfn: String => FileInfo, + ifexists: FileInfo => T, + ifmissing: => T): T = + (try { statfn(file.getPath) } + catch { case SystemError(ENOENT, _) => null }) match { + case null => ifmissing + case st => ifexists(st) + }; + def exists_! : Boolean = statish(stat _, _ => true, false); + def isfifo_! : Boolean = statish(stat _, _.ftype == FIFO, false); + def isblk_! : Boolean = statish(stat _, _.ftype == BLK, false); + def isdir_! : Boolean = statish(stat _, _.ftype == DIR, false); + def ischr_! : Boolean = statish(stat _, _.ftype == CHR, false); + def isreg_! : Boolean = statish(stat _, _.ftype == REG, false); + def islnk_! : Boolean = statish(lstat _, _.ftype == LNK, false); + def issock_! : Boolean = statish(stat _, _.ftype == SOCK, false); + + /* Slightly more cooked file operations. */ + def remove_!() { + /* Delete a file, or directory, whatever it is. */ + try { unlink_!(); return; } + catch { + case SystemError(ENOENT, _) => return; + case SystemError(EISDIR, _) => + try { rmdir_!(); return; } + catch { case SystemError(ENOENT, _) => return; } + } + } + + def rmTree() { + /* Delete a thing recursively. */ + def walk(f: File) { + if (f.isdir_!) f.foreachFile(walk _); + f.remove_!(); + } + walk(file); + } + + def mkdirNew_!() { + /* Make a directory if there's nothing there already. */ + try { mkdir_!(); } + catch { case SystemError(EEXIST, _) => ok; } + } + + /* File locking. */ + def lock_!(flags: Int): FileLock = new FileLock(file.getPath, flags); + def lock_!(): FileLock = lock_!(LKF_EXCL | 0x1b6); + def withLock[T](flags: Int)(body: => T): T = { + val lk = lock_!(flags); + try { body } finally { lk.close(); } + } + def withLock[T](body: => T): T = withLock(LKF_EXCL | 0x1b6) { body }; + + /* Opening files. Again, I'm surprised this isn't here already. */ + def open(): FileInputStream = new FileInputStream(file); + def openForOutput(): FileOutputStream = new FileOutputStream(file); + def reader(): BufferedReader = new BufferedReader(new FileReader(file)); + def writer(): BufferedWriter = new BufferedWriter(new FileWriter(file)); + def withInput[T](body: FileInputStream => T): T = { + val in = open(); + try { body(in) } + finally { in.close(); } + } + def withOutput[T](body: FileOutputStream => T): T = { + val out = openForOutput(); + try { body(out) } finally { out.close(); } + } + def withReader[T](body: BufferedReader => T): T = { + val r = reader(); + try { body(r) } + finally { r.close(); } + } + def withWriter[T](body: BufferedWriter => T): T = { + val w = writer(); + try { body(w) } + finally { w.close(); } + } + } +} +import FileImplicits._; + +/*----- Miscellaneous file hacks ------------------------------------------*/ def freshFile(d: File): File = { /* Return the name of a freshly created file in directory D. */ @@ -261,7 +718,7 @@ def freshFile(d: File): File = { val buf = new Array[Byte](6); val b = new StringBuilder; - while (true) { + loop[File] { exit => /* Keep going until we find a fresh one. */ /* Provide a prefix. Mostly this is to prevent the file starting with @@ -293,34 +750,191 @@ def freshFile(d: File): File = { /* Make the filename, and try to create the file. If we succeed, we * win. */ - val f = new File(d, b.result); b.clear(); - try { jni.mkfile(f); return f; } - catch { case SystemError(EEXIST, _) => (); } + val f = d/b.result; b.clear(); + try { f.mkfile_!(); exit(f); } + catch { case SystemError(EEXIST, _) => ok; } } +} + +/*----- Running a command -------------------------------------------------*/ - /* We shouldn't get here, but the type checker needs placating. */ - unreachable("unreachable"); +private val devnull = new File("/dev/null"); + +private def captureStream(in: InputStream, out: StringBuilder) { + /* Capture the INSTREAM's contents in a string. */ + + for ((buf, n) <- blocks(new InputStreamReader(in))) + out.appendAll(buf, 0, n); +} + +class SubprocessFailed(val cmd: Seq[String], rc: Int, stderr: String) + extends Exception { + override def getMessage(): String = + s"process (${quoteTokens(cmd)}) failed (rc = $rc):\n" + stderr } -def rmTree(f: File) { - def walk(f: File) { - if (jni.stat(f).isdir) { - closing(new jni.DirFilesIterator(f)) { _ foreach(walk _) } - try { jni.rmdir(f); } - catch { case SystemError(ENOENT, _) => (); } - } else { - try { jni.unlink(f); } - catch { case SystemError(ENOENT, _) => (); } +def runCommand(cmd: String*): (String, String) = { + /* Run a command, returning its stdout and stderr. */ + + withCleaner { clean => + + /* Create the child process and pick up the ends of its streams. */ + val pb = new ProcessBuilder(cmd.asJava); + val kid = pb.start(); clean { kid.destroy(); } + kid.getOutputStream.close(); + val out = kid.getInputStream(); clean { out.close(); } + val err = kid.getErrorStream(); clean { err.close(); } + + /* Capture the output in threads, so we don't block. Also, wait for the + * child to complete. Amazingly, messing with threads here isn't too + * much of a disaster. + */ + val bout, berr = new StringBuilder; + val rdout = thread("capture process stdout", daemon = false) { + captureStream(out, bout); + } + val rderr = thread("capture process stderr", daemon = false) { + captureStream(err, berr); + } + val wait = thread("await process exit", daemon = false) { + kid.waitFor(); + } + rdout.join(); rderr.join(); wait.join(); + + /* Check the exit status. */ + val rc = kid.exitValue; + if (rc) throw new SubprocessFailed(cmd, rc, berr.result); + + /* We're all done. */ + return (bout.result, berr.result); + } +} + +/*----- Interrupt triggers ------------------------------------------------*/ + +private val triggerLock = new Object; +private final val maxTriggers = 2; +private var nTriggers = 0; +private var triggers: List[Wrapper] = Nil; + +@native protected def make_trigger(): Wrapper; +@native protected def destroy_trigger(trig: Wrapper); +@native protected def reset_trigger(trig: Wrapper); +@native protected def trigger(trig: Wrapper); + +private def getTrigger(): Wrapper = { + triggerLock synchronized { + if (!nTriggers) + make_trigger() + else { + val trig = triggers.head; + triggers = triggers.tail; + nTriggers -= 1; + trig } } - walk(f); } -def rmTree(path: String) { rmTree(new File(path)); } -def fileExists(path: String): Boolean = - try { jni.stat(path); true } - catch { case SystemError(ENOENT, _) => false }; -def fileExists(file: File): Boolean = fileExists(file.getPath); +private def putTrigger(trig: Wrapper) { + reset_trigger(trig); + triggerLock synchronized { + if (nTriggers >= maxTriggers) + destroy_trigger(trig); + else { + triggers ::= trig; + nTriggers += 1; + } + } +} + +private def withTrigger[T](body: Wrapper => T): T = { + val trig = getTrigger(); + try { body(trig) } + finally { putTrigger(trig); } +} + +def interruptWithTrigger[T](body: Wrapper => T): T = { + /* interruptWithTrigger { TRIG => BODY } + * + * Execute BODY and return its result. If the thread receives an + * interrupt, the trigger TRIG will be pulled. See `interruptably' for the + * full semantics. + */ + + withTrigger { trig => + interruptably { body(trig) } onInterrupt { trigger(trig); } + }; +} + +/*----- Glue for the VPN server -------------------------------------------*/ + +/* The lock class. This is only a class because they're much easier to find + * than loose objects through JNI. + */ +private class ServerLock; + +/* Exceptions. */ +class NameResolutionException(msg: String) extends Exception(msg); +class InitializationException(msg: String) extends Exception(msg); + +/* Primitive operations. */ +@native protected def open_tun(): Int; +@native protected def base_init(); +@native protected def setup_resolver(); +@native def load_keys(priv: CString, pub: CString, tag: CString); +@native def unload_keys(); +@native def bind(host: CString, svc: CString); +@native def unbind(); +@native def mark(seq: Int); +@native def run(); +@native protected def send(buf: CString, start: Int, len: Int, + trig: Wrapper); +@native protected def recv(buf: CString, start: Int, len: Int, + trig: Wrapper): Int; + +base_init(); +setup_resolver(); + +/* Tunnel descriptor plumbing. */ +val pending = HashMap[String, Int](); + +def getTunnelFd(peer: CString): Int = + pending synchronized { pending(peer.toJString) }; +def storeTunnelFd(peer: String, fd: Int) + { pending synchronized { pending(peer) = fd; } } +def withdrawTunnelFd(peer: String) + { pending synchronized { pending -= peer; } } +def withTunnelFd[T](peer: String, fd: Int)(body: => T): T = { + storeTunnelFd(peer, fd); + try { body } finally { withdrawTunnelFd(peer); } +} + +/* Server I/O. */ +lazy val serverInput: InputStream = new InputStream { + override def read(): Int = { + val buf = new Array[Byte](1); + val n = read(buf, 0, 1); + if (n < 0) -1 else buf(0)&0xff; + } + override def read(buf: Array[Byte]): Int = + read(buf, 0, buf.length); + override def read(buf: Array[Byte], start: Int, len: Int) = + interruptWithTrigger { trig => recv(buf, start, len, trig); }; + override def close() { } +} + +lazy val serverOutput: OutputStream = new OutputStream { + override def write(b: Int) { write(Array[Byte](b.toByte), 0, 1); } + override def write(buf: Array[Byte]) { write(buf, 0, buf.length); } + override def write(buf: Array[Byte], start: Int, len: Int) + { interruptWithTrigger { trig => send(buf, start, len, trig); } } + override def close() { } +} + +/*----- Crypto-library hacks ----------------------------------------------*/ + +@native def hashsz(hash: String): Int; + /* Return the output hash size for the named HASH function, or -1. */ /*----- That's all, folks -------------------------------------------------*/