rough work in progress; may not build
[tripe-android] / sys.scala
index c012d1f..cf0a72d 100644 (file)
--- a/sys.scala
+++ b/sys.scala
@@ -27,9 +27,13 @@ package uk.org.distorted.tripe; package object sys {
 
 /*----- Imports -----------------------------------------------------------*/
 
+import scala.collection.convert.decorateAsJava._;
 import scala.collection.mutable.HashSet;
 
-import java.io.{Closeable, File};
+import java.io.{BufferedReader, BufferedWriter, Closeable, File,
+               FileDescriptor, FileInputStream, FileOutputStream,
+               InputStream, InputStreamReader,
+               OutputStream, OutputStreamWriter};
 import java.nio.{ByteBuffer, CharBuffer};
 import java.nio.charset.Charset;
 import java.util.Date;
@@ -58,7 +62,7 @@ object StringImplicits {
       var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
       var out = ByteBuffer.allocate(sz);
 
-      while (true) {
+      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.
         */
@@ -86,12 +90,9 @@ object StringImplicits {
          val v = new Array[Byte](n + 1);
          out.array.copyToArray(v, 0, n);
          v(n) = 0;
-         return v;
+         exit(v);
        }
       }
-
-      /* Placate the type checker. */
-      unreachable("unreachable");
     }
   }
 
@@ -125,37 +126,80 @@ import StringImplicits._;
 /* Import the native code library. */
 System.loadLibrary("toy");
 
-/* Exception indicating that a wrapped native object has been clobbered. */
-class NativeObjectTypeException(msg: String) extends RuntimeException(msg);
+/* 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 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 (ErrorEntry(tag, err) <- errtab) b += tag -> err;
     b.result
   }
-  private var wrong = -255;
-  private val seen = HashSet[Int]();
 
-  class ErrnoVal private[Errno](tag: String, val code: Int, id: Int)
-       extends Val(id, tag) {
+  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 Type private[Errno](tag: String, val code: Int, id: Int)
+         extends Val(id, tag) {
+    /* Our augmented error type. */
+
     def message: String = strerror(code).toJString;
   }
+  private class UnknownError(code: Int)
+         extends Type("<unknown>", code, code);
 
-  private 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) }
+  private def err(tag: String, code: Int): Type = {
+    /* Construct an error symbol given its tag string and a code number. */
+
+    if (code < 0) new Type(tag, code, code)
+    else if (seen contains code) new Type(tag, code, nextwrong)
+    else { seen += code; new Type(tag, code, code) }
+  }
+  private def err(tag: String): Type =
+    err(tag, tagmap.getOrElse(tag, nextwrong));
+
+  def byid(id: Int): Value = {
+    if (seen contains id) apply(id)
+    else new UnknownError(id)
   }
-  private 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
@@ -333,23 +377,25 @@ object Errno extends Enumeration {
   val EHWPOISON = err("EHWPOISON");
   /***end***/
 }
-import Errno.{Value => _, _};
+import Errno.{Type => 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. */
 
-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); }
+  /* A constructor which takes an error number, for easier access from C. */
   private def this(err: Int, what: CString)
-    { this(Errno(err), what.toJString); }
+    { this(Errno.byid(err).asInstanceOf[Errno], what.toJString); }
+
   override def getMessage(): String = s"$what: ${err.message}";
 }
 
@@ -367,32 +413,37 @@ def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); }
 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.
  */
-val S_IFMT = 0xf000;
-val S_IFIFO = 0x1000;
-val S_IFCHR = 0x2000;
-val S_IFDIR = 0x4000;
-val S_IFBLK = 0x6000;
-val S_IFREG = 0x8000;
-val S_IFLNK = 0xa000;
-val S_IFSOCK = 0xc000;
-
+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. */
   val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, 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,
@@ -401,18 +452,26 @@ class FileInfo private[this](val devMajor: Int, val devMinor: Int,
                             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
@@ -425,29 +484,48 @@ class FileInfo private[this](val devMajor: Int, val devMinor: Int,
   }
 
   private[this] def mustBeDevice() {
+    /* Insist that you only ask for `rdev' fields on actual device nodes. */
     ftype match {
-      case CHR | BLK => ();
+      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(); }
-  private[this] val dir = opendir(cpath);
+
+  /* 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 {
@@ -458,29 +536,47 @@ protected abstract class BaseDirIterator[T](cpath: CString)
 }
 
 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 ------------------------------------------------------*/
 
-val LKF_EXCL = 1;
-val LKF_WAIT = 2;
-@native protected def lock(path: CString, flags: Int): Wrapper;
+/* 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); }
-  def this(file: File) { this(file.getPath, LKF_EXCL); }
+  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(); }
 }
@@ -489,23 +585,41 @@ class FileLock(path: String, flags: Int) extends Closeable {
 
 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 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 =
@@ -524,33 +638,63 @@ object FileImplicits {
     def issock_! : Boolean = statish(stat _, _.ftype == SOCK, false);
 
     def remove_!() {
+      /* Delete a file, or directory, whatever it is. */
       while (true) {
-       try { unlink_!(); return }
+       try { unlink_!(); return; }
        catch {
          case SystemError(ENOENT, _) => return;
-         case SystemError(EISDIR, _) => ();
+         case SystemError(EISDIR, _) => ok;
        }
-       try { rmdir_!(); return }
+       try { rmdir_!(); return; }
        catch {
          case SystemError(ENOENT, _) => return;
-         case SystemError(ENOTDIR, _) => ();
+         case SystemError(ENOTDIR, _) => ok;
        }
       }
     }
 
     def rmTree() {
+      /* Delete a thing recursively. */
       def walk(f: File) {
-       if (f.isdir_!) f.withFilesIterator { _ foreach(walk _) };
+       if (f.isdir_!) f.foreachFile(walk _);
        f.remove_!();
       }
       walk(file);
     }
 
+    /* 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 = new FileLock(file.getPath, flags);
+      val lk = lock_!(flags);
       try { body } finally { lk.close(); }
     }
-    def withLock[T](body: => T): T = withLock(LKF_EXCL) { body };
+    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 InputStreamReader(open()));
+    def writer(): BufferedWriter =
+      new BufferedWriter(new OutputStreamWriter(openForOutput()));
+    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 = withInput { in =>
+      body(new BufferedReader(new InputStreamReader(in)))
+    };
+    def withWriter[T](body: BufferedWriter => T): T = withOutput { out =>
+      val w = new BufferedWriter(new OutputStreamWriter(out));
+      /* Do this the hard way, so that we flush the `BufferedWriter'. */
+      try { body(w) } finally { w.close(); }
+    }
   }
 }
 import FileImplicits._;
@@ -563,7 +707,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
@@ -596,19 +740,70 @@ def freshFile(d: File): File = {
      * win.
      */
     val f = new File(d, b.result); b.clear();
-    try { f.mkfile_!(); return f; }
-    catch { case SystemError(EEXIST, _) => (); }
+    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 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).redirectInput(devnull);
+    val kid = pb.start(); clean { kid.destroy(); }
+    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 != 0) throw new SubprocessFailed(cmd, rc, berr.result);
+
+    /* We're all done. */
+    return (bout.result, berr.result);
+  }
 }
 
 /*----- Connecting to a server --------------------------------------------*/
 
-val CF_CLOSERD = 1;
-val CF_CLOSEWR = 2;
-val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
+/* Primitive operations. */
+final val CF_CLOSERD = 1;
+final val CF_CLOSEWR = 2;
+final val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
 @native protected def connect(path: CString): Wrapper;
 @native protected def send(conn: Wrapper, buf: CString,
                           start: Int, len: Int);
@@ -617,12 +812,20 @@ val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
 @native def closeconn(conn: Wrapper, how: Int);
 
 class Connection(path: String) extends Closeable {
-  def this(file: File) { this(file.getPath); }
+
+  /* The underlying primitive connection. */
   private[this] val conn = connect(path.toCString);
+
+  /* Alternative constructors. */
+  def this(file: File) { this(file.getPath); }
+
+  /* Cleanup.*/
   override def close() { closeconn(conn, CF_CLOSEMASK); }
   override protected def finalize() { super.finalize(); close(); }
 
-  class InputStream private[Connection] extends java.io.InputStream {
+  class Input private[Connection] extends InputStream {
+    /* An input stream which reads from the connection. */
+
     override def read(): Int = {
       val buf = new Array[Byte](1);
       val n = read(buf, 0, 1);
@@ -634,16 +837,18 @@ class Connection(path: String) extends Closeable {
       recv(conn, buf, start, len);
     override def close() { closeconn(conn, CF_CLOSERD); }
   }
-  lazy val input = new InputStream;
+  lazy val input = new Input;
+
+  class Output private[Connection] extends OutputStream {
+    /* An output stream which writes to the connection. */
 
-  class OutputStream private[Connection] extends java.io.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)
       { send(conn, buf, start, len); }
     override def close() { closeconn(conn, CF_CLOSEWR); }
   }
-  lazy val output = new OutputStream;
+  lazy val output = new Output;
 }
 
 /*----- Crypto-library hacks ----------------------------------------------*/