Fix switching between raw and canonical input

There were a number of issues with swithcing between raw and canonical
issues that affected both the server and the thin client. These were
reported in #5863 and #5856. In both cases, there were issues with
reading input or having the input be displayed. Debugging those issues
revealed a number of issues with how we were using the jline 3 system
terminal and the hybrid interaction with the jline 2 terminal. This
commit eliminates all of our internal jline 2 usage. The only remaining
jline 2 usage is that we create and override the global terminal for the
scala console for scala versions < 2.13. By moving away from jline 2, I
was also able to fix #5828, which reported that the home, end and delete
keys were not working.

One of the big issues that this commit addresses is that the
NetworkClient was always performing blocking reads on System.in. This
was problematic because it turns out that you can't switch between raw
and canonical modes when there is a read present. To fix this, the
server now sends a message to the client when it wants to read bytes and
only then does the client create a background thread to read a single
byte.

I also figured out how to set the terminal type properly for the thin
client on windows where we had been manually setting the capabilities to
ansi, which only worked for some keys. This fix required switching to
the WindowsInputStream that I introduced in a prior commit. Before we
were using the jline 2 wrapped input stream which was converting some
system events, like home and end, to the wrong escape sequence mappings.

The remainder of the commit is mostly just converting from jline 2 apis
to jline 3 apis.

I verified that tab completions, the scala console, the ammonite console
and a run task that read from System.in all work with both the server
and the thin client on mac, linux and windows after these changes.

Fixes #5828, #5863, #5856
This commit is contained in:
Ethan Atkins
2020-09-21 13:42:03 -07:00
parent 410a8dd4b1
commit bb8b9a1c99
25 changed files with 609 additions and 241 deletions
@@ -85,6 +85,7 @@ public class BootServerSocket implements AutoCloseable {
private final Object lock = new Object();
private final LinkedBlockingQueue<ClientSocket> clientSocketReads = new LinkedBlockingQueue<>();
private final Path socketFile;
private final AtomicBoolean needInput = new AtomicBoolean(false);
private class ClientSocket implements AutoCloseable {
final Socket socket;
@@ -116,13 +117,20 @@ public class BootServerSocket implements AutoCloseable {
final InputStream inputStream = socket.getInputStream();
while (alive.get()) {
try {
int b = inputStream.read();
if (b != -1) {
bytes.put(b);
clientSocketReads.put(ClientSocket.this);
} else {
alive.set(false);
synchronized (needInput) {
while (!needInput.get() && alive.get()) needInput.wait();
}
if (alive.get()) {
socket.getOutputStream().write(5);
int b = inputStream.read();
if (b != -1) {
bytes.put(b);
clientSocketReads.put(ClientSocket.this);
} else {
alive.set(false);
}
}
} catch (IOException e) {
alive.set(false);
}
@@ -209,10 +217,18 @@ public class BootServerSocket implements AutoCloseable {
@Override
public int read() {
try {
synchronized (needInput) {
needInput.set(true);
needInput.notifyAll();
}
ClientSocket clientSocket = clientSocketReads.take();
return clientSocket.bytes.take();
} catch (final InterruptedException e) {
return -1;
} finally {
synchronized (needInput) {
needInput.set(false);
}
}
}
};
@@ -38,8 +38,10 @@ import scala.util.{ Failure, Properties, Success, Try }
import Serialization.{
CancelAll,
attach,
cancelReadSystemIn,
cancelRequest,
promptChannel,
readSystemIn,
systemIn,
systemErr,
systemOut,
@@ -50,6 +52,8 @@ import Serialization.{
terminalGetSize,
terminalPropertiesQuery,
terminalPropertiesResponse,
terminalSetEcho,
terminalSetRawMode,
terminalSetSize,
getTerminalAttributes,
setTerminalAttributes,
@@ -149,11 +153,15 @@ class NetworkClient(
private[this] val stdinBytes = new LinkedBlockingQueue[Int]
private[this] val inLock = new Object
private[this] val inputThread = new AtomicReference(new RawInputThread)
private[this] val inputThread = new AtomicReference[RawInputThread]
private[this] val exitClean = new AtomicBoolean(true)
private[this] val sbtProcess = new AtomicReference[Process](null)
private class ConnectionRefusedException(t: Throwable) extends Throwable(t)
private class ServerFailedException extends Exception
private[this] def startInputThread(): Unit = inputThread.get match {
case null => inputThread.set(new RawInputThread)
case _ =>
}
// Open server connection based on the portfile
def init(promptCompleteUsers: Boolean, retry: Boolean): ServerConnection =
@@ -165,6 +173,7 @@ class NetworkClient(
if (noStdErr) System.exit(0)
else if (noTab) waitForServer(portfile, log = true, startServer = true)
else {
startInputThread()
stdinBytes.take match {
case 9 =>
errorStream.println("\nStarting server...")
@@ -250,8 +259,13 @@ class NetworkClient(
Option(inputThread.get).foreach(_.close())
Option(interactiveThread.get).foreach(_.interrupt)
}
case "readInput" =>
case _ => self.onNotification(msg)
case `readSystemIn` => startInputThread()
case `cancelReadSystemIn` =>
inputThread.get match {
case null =>
case t => t.close()
}
case _ => self.onNotification(msg)
}
}
override def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg)
@@ -289,9 +303,10 @@ class NetworkClient(
var socket: Option[Socket] =
if (!Properties.isLinux) Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption
else None
val term = Terminal.console
term.exitRawMode()
val process = socket match {
case None if startServer =>
val term = Terminal.console
if (log) console.appendLog(Level.Info, "server was not detected. starting an instance")
val props =
@@ -349,6 +364,7 @@ class NetworkClient(
s.getInputStream.read match {
case -1 | 0 => readThreadAlive.set(false)
case 2 => gotInputBack = true
case 5 => term.enterRawMode(); startInputThread()
case 3 if gotInputBack => readThreadAlive.set(false)
case i if gotInputBack => stdinBytes.offer(i)
case i => printStream.write(i)
@@ -381,9 +397,6 @@ class NetworkClient(
while (!gotInputBack && !stdinBytes.isEmpty && socket.isDefined) {
val out = s.getOutputStream
val b = stdinBytes.poll
// echo stdin during boot
printStream.write(b)
printStream.flush()
out.write(b)
out.flush()
}
@@ -610,18 +623,13 @@ class NetworkClient(
case (`terminalCapabilities`, Some(json)) =>
Converter.fromJson[TerminalCapabilitiesQuery](json) match {
case Success(terminalCapabilitiesQuery) =>
val jline3 = terminalCapabilitiesQuery.jline3
val response = TerminalCapabilitiesResponse(
terminalCapabilitiesQuery.boolean
.map(Terminal.console.getBooleanCapability(_, jline3)),
.map(Terminal.console.getBooleanCapability(_)),
terminalCapabilitiesQuery.numeric
.map(
c => Option(Terminal.console.getNumericCapability(c, jline3)).fold(-1)(_.toInt)
),
.map(c => Option(Terminal.console.getNumericCapability(c)).fold(-1)(_.toInt)),
terminalCapabilitiesQuery.string
.map(
s => Option(Terminal.console.getStringCapability(s, jline3)).getOrElse("null")
),
.map(s => Option(Terminal.console.getStringCapability(s)).getOrElse("null")),
)
sendCommandResponse(
terminalCapabilitiesResponse,
@@ -677,6 +685,21 @@ class NetworkClient(
sendCommandResponse("", TerminalSetSizeResponse(), msg.id)
case Failure(_) =>
}
case (`terminalSetEcho`, Some(json)) =>
Converter.fromJson[TerminalSetEchoCommand](json) match {
case Success(echo) =>
Terminal.console.setEchoEnabled(echo.toggle)
sendCommandResponse("", TerminalSetEchoResponse(), msg.id)
case Failure(_) =>
}
case (`terminalSetRawMode`, Some(json)) =>
Converter.fromJson[TerminalSetRawModeCommand](json) match {
case Success(raw) =>
if (raw.toggle) Terminal.console.enterRawMode()
else Terminal.console.exitRawMode()
sendCommandResponse("", TerminalSetRawModeResponse(), msg.id)
case Failure(_) =>
}
case _ =>
}
}
@@ -787,6 +810,7 @@ class NetworkClient(
else if (noTab) updateCompletions()
else {
errorStream.print(s"\nNo cached $label names found. Press '<tab>' to compile: ")
startInputThread()
stdinBytes.take match {
case 9 => updateCompletions()
case _ => Nil
@@ -901,17 +925,18 @@ class NetworkClient(
start()
val stopped = new AtomicBoolean(false)
override final def run(): Unit = {
@tailrec def read(): Unit = {
def read(): Unit = {
inputStream.read match {
case -1 =>
case b =>
inLock.synchronized(stdinBytes.offer(b))
if (attached.get()) drain()
if (!stopped.get()) read()
}
}
try Terminal.console.withRawInput(read())
catch { case _: InterruptedException | NonFatal(_) => stopped.set(true) }
try read()
catch { case _: InterruptedException | NonFatal(_) => stopped.set(true) } finally {
inputThread.set(null)
}
}
def drain(): Unit = inLock.synchronized {
@@ -1095,6 +1120,7 @@ object NetworkClient {
System.out.flush()
})
Runtime.getRuntime.addShutdownHook(hook)
if (Util.isNonCygwinWindows) sbt.internal.util.JLine3.forceWindowsJansi()
System.exit(Terminal.withStreams(false) {
val term = Terminal.console
try client(base, restOfArgs, term.inputStream, System.err, term, useJNI)