From f3b3148c5898d13593cc0531fd618346ad38731d Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Wed, 18 Nov 2020 12:47:28 -0800 Subject: [PATCH 1/2] Use NetworkClient to implement `sbt -bsp` Network client already supports the -bsp command (since 65ab7c94d038955d48eefbf7951fb9cd399ab7ef). This commit reworks the BspClient.run method so that it delegates to the NetworkClient. The advantage to doing it this way is that improvements to starting up the sbt server by the thin client will automatically propagate to the -bsp command. The way that it is implemented, all of the output generated during server startup will be redirected to System.err which is useful for debugging without messing up the bsp protocol, which relies on only bsp messages being written to System.out. --- .../scala/sbt/internal/client/BspClient.scala | 58 +------------------ .../sbt/internal/client/NetworkClient.scala | 32 +++++----- main/src/main/scala/sbt/Main.scala | 10 ++-- 3 files changed, 27 insertions(+), 73 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/client/BspClient.scala b/main-command/src/main/scala/sbt/internal/client/BspClient.scala index c9942ba99..19b8e106a 100644 --- a/main-command/src/main/scala/sbt/internal/client/BspClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/BspClient.scala @@ -7,20 +7,12 @@ package sbt.internal.client -import java.io.{ File, InputStream, OutputStream } +import java.io.{ InputStream, OutputStream } import java.net.Socket import java.util.concurrent.atomic.AtomicBoolean import sbt.Exit -import sbt.io.syntax._ -import sbt.protocol.ClientSocket - import scala.util.control.NonFatal -import java.lang.ProcessBuilder.Redirect - -class BspClient private (sbtServer: Socket) { - private def run(): Exit = Exit(BspClient.bspRun(sbtServer)) -} object BspClient { private[sbt] def bspRun(sbtServer: Socket): Int = { @@ -72,52 +64,6 @@ object BspClient { thread } def run(configuration: xsbti.AppConfiguration): Exit = { - val baseDirectory = configuration.baseDirectory - val portFile = baseDirectory / "project" / "target" / "active.json" - try { - if (!portFile.exists) { - forkServer(baseDirectory, portFile) - } - val (socket, _) = ClientSocket.socket(portFile) - new BspClient(socket).run() - } catch { - case NonFatal(_) => Exit(1) - } - } - - /** - * Forks another instance of sbt in the background. - * This instance must be shutdown explicitly via `sbt -client shutdown` - */ - def forkServer(baseDirectory: File, portfile: File): Unit = { - val args = List("--detach-stdio") - val launchOpts = List( - "-Dfile.encoding=UTF-8", - "-Dsbt.io.virtual=true", - "-Xms1024M", - "-Xmx1024M", - "-Xss4M", - "-XX:ReservedCodeCacheSize=128m" - ) - - val launcherJarString = sys.props.get("java.class.path") match { - case Some(cp) => - cp.split(File.pathSeparator) - .headOption - .getOrElse(sys.error("launcher JAR classpath not found")) - case _ => sys.error("property java.class.path expected") - } - - val cmd = "java" :: launchOpts ::: "-jar" :: launcherJarString :: args - val processBuilder = - new ProcessBuilder(cmd: _*) - .directory(baseDirectory) - .redirectInput(Redirect.PIPE) - - val process = processBuilder.start() - - while (process.isAlive && !portfile.exists) Thread.sleep(100) - - if (!process.isAlive) sys.error("sbt server exited") + Exit(NetworkClient.run(configuration, configuration.arguments.toList, redirectOutput = true)) } } 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 c89715661..ccf33bbe4 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -1093,16 +1093,20 @@ object NetworkClient { terminal: Terminal, useJNI: Boolean ): Int = { + val printStream = if (args.bsp) errorStream else terminal.printStream val client = simpleClient( args.withBaseDirectory(baseDirectory), inputStream, + printStream, errorStream, useJNI, - terminal ) + clientImpl(client, args.bsp) + } + private def clientImpl(client: NetworkClient, isBsp: Boolean): Int = { try { - if (args.bsp) { + if (isBsp) { val (socket, _) = client.connectOrStartServerAndConnect(promptCompleteUsers = false, retry = true) BspClient.bspRun(socket) @@ -1214,16 +1218,18 @@ object NetworkClient { } def run(configuration: xsbti.AppConfiguration, arguments: List[String]): Int = - try { - val client = new NetworkClient(configuration, parseArgs(arguments.toArray)) - try { - if (client.connect(log = true, promptCompleteUsers = false)) client.run() - else 1 - } catch { case _: Throwable => 1 } finally client.close() - } catch { - case NonFatal(e) => - e.printStackTrace() - 1 - } + run(configuration, arguments, false) + def run( + configuration: xsbti.AppConfiguration, + arguments: List[String], + redirectOutput: Boolean + ): Int = { + val term = Terminal.console + val err = new PrintStream(term.errorStream) + val out = if (redirectOutput) err else new PrintStream(term.outputStream) + val args = parseArgs(arguments.toArray).withBaseDirectory(configuration.baseDirectory) + val client = simpleClient(args, term.inputStream, out, err, useJNI = false) + clientImpl(client, args.bsp) + } private class AccessDeniedException extends Throwable } diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index d7b857fa4..1f5888c78 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -64,10 +64,6 @@ private[sbt] object xMain { import sbt.internal.CommandStrings.{ BootCommand, DefaultsCommand, InitCommand } import sbt.internal.client.NetworkClient - val bootServerSocket = getSocketOrExit(configuration) match { - case (_, Some(e)) => return e - case (s, _) => s - } // if we detect -Dsbt.client=true or -client, run thin client. val clientModByEnv = SysProp.client val userCommands = configuration.arguments @@ -75,6 +71,12 @@ private[sbt] object xMain { .filterNot(_ == DashDashServer) val isClient: String => Boolean = cmd => (cmd == DashClient) || (cmd == DashDashClient) val isBsp: String => Boolean = cmd => (cmd == "-bsp") || (cmd == "--bsp") + val isServer = !userCommands.exists(c => isBsp(c) || isClient(c)) + val bootServerSocket = if (isServer) getSocketOrExit(configuration) match { + case (_, Some(e)) => return e + case (s, _) => s + } + else None if (userCommands.exists(isBsp)) { BspClient.run(dealiasBaseDirectory(configuration)) } else { From 7a8a5e5dac254ebf4a6c6f62f90ba0d2fecff16e Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Thu, 19 Nov 2020 08:23:15 -0800 Subject: [PATCH 2/2] Disable console ui for client launched server If the sbt server is launched by the remote client, it should not have a console ui thread because there is no way to even feed input to it once the server has launched. Having the ui thread can cause the server to exit unexpectedly if an EOF is read from the console input stream. --- main/src/main/scala/sbt/internal/CommandExchange.scala | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 29f34f7da..3569287bb 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -51,7 +51,6 @@ private[sbt] final class CommandExchange { private var server: Option[ServerInstance] = None private val firstInstance: AtomicBoolean = new AtomicBoolean(true) private val monitoringActiveJson: AtomicBoolean = new AtomicBoolean(false) - private var consoleChannel: Option[ConsoleChannel] = None private val commandQueue: LinkedBlockingQueue[Exec] = new LinkedBlockingQueue[Exec] private val channelBuffer: ListBuffer[CommandChannel] = new ListBuffer() private val channelBufferLock = new AnyRef {} @@ -60,6 +59,7 @@ private[sbt] final class CommandExchange { private[this] val lastState = new AtomicReference[State] private[this] val currentExecRef = new AtomicReference[Exec] private[sbt] def hasServer = server.isDefined + addConsoleChannel() def channels: List[CommandChannel] = channelBuffer.toList @@ -141,11 +141,9 @@ private[sbt] final class CommandExchange { } private def addConsoleChannel(): Unit = - if (consoleChannel.isEmpty) { + if (!Terminal.startedByRemoteClient) { val name = ConsoleChannel.defaultName - val console0 = new ConsoleChannel(name, mkAskUser(name)) - consoleChannel = Some(console0) - subscribe(console0) + subscribe(new ConsoleChannel(name, mkAskUser(name))) } def run(s: State): State = run(s, s.get(autoStartServer).getOrElse(true)) def run(s: State, autoStart: Boolean): State = { @@ -402,7 +400,6 @@ private[sbt] final class CommandExchange { .withChannelName(currentExec.flatMap(_.source.map(_.channelName))) case _ => pe } - if (channels.isEmpty) addConsoleChannel() channels.foreach(c => ProgressState.updateProgressState(newPE, c.terminal)) }