+/* -*-scala-*-
+ *
+ * Managing TrIPE administration connections
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object admin {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{BufferedReader, InputStreamReader, OutputStreamWriter};
+import java.util.concurrent.locks.{Condition, ReentrantLock => Lock};
+
+import scala.collection.mutable.{HashMap, Publisher};
+import scala.concurrent.Channel;
+import scala.util.control.Breaks;
+
+import Implicits._;
+import sys.{serverInput, serverOutput};
+
+/*----- Classification of server messages ---------------------------------*/
+
+sealed abstract class Message;
+
+sealed abstract class JobMessage extends Message;
+case object JobOK extends JobMessage;
+final case class JobInfo(info: Seq[String]) extends JobMessage;
+final case class JobFail(err: Seq[String]) extends JobMessage;
+case object JobLostConnection extends JobMessage;
+
+final case class BackgroundJobMessage(tag: String, msg: JobMessage)
+ extends Message;
+final case class JobDetached(tag: String) extends Message;
+
+sealed abstract class AsyncMessage extends Message;
+final case class Trace(msg: String) extends AsyncMessage;
+final case class Warning(err: Seq[String]) extends AsyncMessage;
+final case class Notify(note: Seq[String]) extends AsyncMessage;
+case object ConnectionLost extends AsyncMessage;
+
+sealed abstract class ServiceMessage extends Message;
+final case class ServiceCancel(jobid: String) extends ServiceMessage;
+final case class ServiceClaim(svc: String, version: String)
+ extends ServiceMessage;
+final case class ServiceJob(jobid: String, svc: String,
+ cmd: String, args: Seq[String])
+ extends ServiceMessage;
+
+/*----- Main code ---------------------------------------------------------*/
+
+class ConnectionClosed extends Exception;
+
+class ServerFailed(msg: String) extends Exception(msg);
+
+class CommandFailed(val msg: Seq[String]) extends Exception {
+ override def getMessage(): String =
+ "%s(%s)".format(getClass.getName, quoteTokens(msg));
+}
+
+class ConnectionLostException extends Exception;
+
+object Connection extends Publisher[AsyncMessage]
+{
+ /* Synchronization.
+ *
+ * This class is complicatedly multithreaded. The following fields must
+ * only be accessed while the instance is locked. To prevent deadlocks,
+ * hold the `Connection' lock before locking any individual `Job' objects.
+ */
+
+ private var livep: Boolean = true; // Is this connection still alive?
+ private var fgjob: Option[this.Job] = None; // Foreground job, if there is one.
+ private val jobmap = new HashMap[String, this.Job]; // Maps tags to extant jobs.
+ private var bgseq = 0; // Next background job tag.
+
+ private val in = new BufferedReader(new InputStreamReader(serverInput));
+ private val out = new OutputStreamWriter(serverOutput);
+
+ type Pub = Connection.type;
+
+ class Job extends Iterator[Seq[String]] {
+ private[Connection] val ch = new Channel[JobMessage];
+ private[this] var nextmsg: Option[JobMessage] = None;
+
+ private[this] def fetchNext()
+ { if (nextmsg == None) nextmsg = Some(ch.read); }
+ override def hasNext: Boolean = {
+ fetchNext();
+ nextmsg match {
+ case Some(JobOK) => false
+ case _ => true
+ }
+ }
+ override def next(): Seq[String] = {
+ fetchNext();
+ nextmsg match {
+ case None => ???
+ case Some(JobOK) => throw new NoSuchElementException
+ case Some(JobFail(msg)) => throw new CommandFailed(msg)
+ case Some(JobLostConnection) => throw new ConnectionLostException
+ case Some(JobInfo(msg)) => nextmsg = None; msg