From e77906445d6796603d9d82610714bbae53c7a84f Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 24 Jun 2020 18:09:39 -0700 Subject: [PATCH] Refactor network client Neither The ConsoleAppender class nor the jni based ClientSocket can be used in a graalvm native image. This commit reworks the NetworkClient so that we can avoid those limitations. It also adds some additional command line argument parsing and changes the value of the run method to return Int rather than Unit for exit code support. --- .../sbt/internal/client/NetworkClient.scala | 121 ++++++++++++++++-- 1 file changed, 107 insertions(+), 14 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 4728fe345..b7cd0ad92 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -9,13 +9,13 @@ package sbt package internal package client -import java.io.{ File, IOException } +import java.io.{ File, IOException, InputStream, PrintStream } import java.util.UUID import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams } import sbt.internal.protocol._ -import sbt.internal.util.{ ConsoleAppender, LineReader } +import sbt.internal.util.{ ConsoleAppender, ConsoleOut, LineReader } import sbt.io.IO import sbt.io.syntax._ import sbt.protocol._ @@ -23,19 +23,51 @@ import sbt.util.Level import sjsonnew.support.scalajson.unsafe.Converter import scala.collection.mutable.ListBuffer +import scala.collection.mutable import scala.sys.process.{ BasicIO, Process, ProcessLogger } +import scala.util.Properties import scala.util.control.NonFatal import scala.util.{ Failure, Success } +import NetworkClient.Arguments -class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[String]) { self => - private val channelName = new AtomicReference("_") +trait ConsoleInterface { + def appendLog(level: Level.Value, message: => String): Unit + def success(msg: String): Unit +} + +class NetworkClient( + console: ConsoleInterface, + arguments: Arguments, + inputStream: InputStream, + errorStream: PrintStream, + printStream: PrintStream, + useJNI: Boolean, +) extends AutoCloseable { self => + def this(configuration: xsbti.AppConfiguration, arguments: Arguments) = + this( + console = NetworkClient.consoleAppenderInterface(System.out), + arguments = arguments.withBaseDirectory(configuration.baseDirectory), + inputStream = System.in, + errorStream = System.err, + printStream = System.out, + useJNI = false, + ) + def this(configuration: xsbti.AppConfiguration, args: List[String]) = + this( + console = NetworkClient.consoleAppenderInterface(System.out), + arguments = + NetworkClient.parseArgs(args.toArray).withBaseDirectory(configuration.baseDirectory), + inputStream = System.in, + errorStream = System.err, + printStream = System.out, + useJNI = false, + ) private val status = new AtomicReference("Ready") private val lock: AnyRef = new AnyRef {} private val running = new AtomicBoolean(true) private val pendingExecIds = ListBuffer.empty[String] - private val console = ConsoleAppender("thin1") - private def baseDirectory: File = configuration.baseDirectory + private def baseDirectory: File = arguments.baseDirectory lazy val connection = init() @@ -196,9 +228,7 @@ class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[Strin def start(): Unit = { console.appendLog(Level.Info, "entering *experimental* thin client - BEEP WHIRR") val _ = connection - val userCommands = arguments filterNot { cmd => - cmd.startsWith("-") - } + val userCommands = arguments.commandArguments.toList if (userCommands.isEmpty) shell() else batchExecute(userCommands) } @@ -258,14 +288,77 @@ class NetworkClient(configuration: xsbti.AppConfiguration, arguments: List[Strin status.set("Processing") } } + override def close(): Unit = {} } - object NetworkClient { - def run(configuration: xsbti.AppConfiguration, arguments: List[String]): Unit = + private def consoleAppenderInterface(printStream: PrintStream): ConsoleInterface = { + val appender = ConsoleAppender("thin", ConsoleOut.printStreamOut(printStream)) + new ConsoleInterface { + override def appendLog(level: Level.Value, message: => String): Unit = + appender.appendLog(level, message) + override def success(msg: String): Unit = appender.success(msg) + } + } + private def simpleConsoleInterface(printStream: PrintStream): ConsoleInterface = + new ConsoleInterface { + import scala.Console.{ GREEN, RED, RESET, YELLOW } + override def appendLog(level: Level.Value, message: => String): Unit = { + val prefix = level match { + case Level.Error => s"[$RED$level$RESET]" + case Level.Warn => s"[$YELLOW$level$RESET]" + case _ => s"[$RESET$level$RESET]" + } + message.split("\n").foreach { line => + if (!line.trim.isEmpty) printStream.println(s"$prefix $line") + } + } + override def success(msg: String): Unit = printStream.println(s"[${GREEN}success$RESET] $msg") + } + private[client] class Arguments( + val baseDirectory: File, + val sbtArguments: Seq[String], + val commandArguments: Seq[String], + val sbtScript: String, + ) { + def withBaseDirectory(file: File): Arguments = + new Arguments(file, sbtArguments, commandArguments, sbtScript) + } + private[client] def parseArgs(args: Array[String]): Arguments = { + var i = 0 + var sbtScript = if (Properties.isWin) "sbt.cmd" else "sbt" + val commandArgs = new mutable.ArrayBuffer[String] + val sbtArguments = new mutable.ArrayBuffer[String] + val SysProp = "-D([^=]+)=(.*)".r + val sanitized = args.flatMap { + case a if a.startsWith("\"") => Array(a) + case a => a.split(" ") + } + while (i < sanitized.length) { + sanitized(i) match { + case a if a.startsWith("--sbt-script=") => + sbtScript = a.split("--sbt-script=").lastOption.getOrElse(sbtScript) + case a if !a.startsWith("-") => commandArgs += a + case a @ SysProp(key, value) => + System.setProperty(key, value) + sbtArguments += a + case a => + sbtArguments += a + } + i += 1 + } + new Arguments(new File("").getCanonicalFile, sbtArguments, commandArgs, sbtScript) + } + + def run(configuration: xsbti.AppConfiguration, arguments: List[String]): Int = try { - new NetworkClient(configuration, arguments) - () + val client = new NetworkClient(configuration, parseArgs(arguments.toArray)) + try { + client.start() + 0 + } catch { case _: Throwable => 1 } finally client.close() } catch { - case NonFatal(e) => println(e.getMessage) + case NonFatal(e) => + e.printStackTrace() + 1 } }