From 2e3a1e767d35b9008cdaa11a87ce6485c0487050 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 17 Dec 2019 12:46:14 -0800 Subject: [PATCH] Don't poll System.in in ConsoleChannel The ask user thread is a background thread so it's fine for it to block on System.in. By blocking rather than polling, the cpu utilization of sbt drops to 0 on idle. We have to explicitly handle if we block though because the JLine console reader will return null both if the input stream returns -1 --- .../scala/sbt/internal/util/LineReader.scala | 2 ++ .../scala/sbt/internal/util/Terminal.scala | 13 ++++++++++ .../scala/sbt/internal/ConsoleChannel.scala | 24 ++++++++++++------- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala index b8704c97c..660406559 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala @@ -94,6 +94,7 @@ private[sbt] object JLine { @deprecated("Handled by Terminal.fixTerminalProperty", "1.4.0") private[sbt] def fixTerminalProperty(): Unit = () + @deprecated("For binary compatibility only", "1.4.0") private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream = if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds) else originalIn @@ -176,6 +177,7 @@ final class FullReader( val handleCONT: Boolean, inputStream: InputStream, ) extends JLine { + @deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0") def this( historyPath: Option[File], complete: Parser[_], diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index 5322b5869..44a15caf4 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -8,6 +8,7 @@ package sbt.internal.util import java.io.{ InputStream, OutputStream } +import java.nio.channels.ClosedChannelException import java.util.Locale import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import java.util.concurrent.locks.ReentrantLock @@ -54,6 +55,18 @@ object Terminal { */ def systemInIsAttached: Boolean = attached.get + /** + * Returns an InputStream that will throw a [[ClosedChannelException]] if read returns -1. + * @return the wrapped InputStream. + */ + private[sbt] def throwOnClosedSystemIn: InputStream = new InputStream { + override def available(): Int = WrappedSystemIn.available() + override def read(): Int = WrappedSystemIn.read() match { + case -1 => throw new ClosedChannelException + case r => r + } + } + /** * Provides a wrapper around System.in. The wrapped stream in will check if the terminal is attached * in available and read. If a read returns -1, it will mark System.in as unattached so that diff --git a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala index 12c4bfcce..f18fa2981 100644 --- a/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala +++ b/main-command/src/main/scala/sbt/internal/ConsoleChannel.scala @@ -8,12 +8,14 @@ package sbt package internal -import sbt.internal.util._ -import BasicKeys._ import java.io.File +import java.nio.channels.ClosedChannelException + +import sbt.BasicKeys._ +import sbt.internal.util.Util.AnyOps +import sbt.internal.util._ import sbt.protocol.EventMessage import sjsonnew.JsonFormat -import Util.AnyOps private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel { private var askUserThread: Option[Thread] = None @@ -23,13 +25,19 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel case Some(pf) => pf(s) case None => "> " } - val reader = new FullReader(history, s.combinedParser, JLine.HandleCONT, true) + val reader = + new FullReader(history, s.combinedParser, JLine.HandleCONT, Terminal.throwOnClosedSystemIn) override def run(): Unit = { // This internally handles thread interruption and returns Some("") - val line = reader.readLine(prompt) - line match { - case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) - case None => append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name)))) + try { + reader.readLine(prompt) match { + case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) + case None => + println("") // Prevents server shutdown log lines from appearing on the prompt line + append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name)))) + } + } catch { + case _: ClosedChannelException => } askUserThread = None }