Consolidate and optimize input stream json reading

We had similar code for reading json frames from an input stream in
NetworkChannel and ServerConnection. I reworked and consolidated this
logic into a shared method in ReadJsonFromInputStream.

This commit also removes the ObjectMessage reporting methods that
weren't doing anything.
This commit is contained in:
Ethan Atkins
2020-06-24 19:19:06 -07:00
parent b0a859acb5
commit fcfe4333fe
5 changed files with 122 additions and 222 deletions
@@ -9,10 +9,13 @@ package sbt
package internal
package client
import java.net.{ SocketTimeoutException, Socket }
import java.io.IOException
import java.net.{ Socket, SocketTimeoutException }
import java.util.concurrent.atomic.AtomicBoolean
import sbt.protocol._
import sbt.internal.protocol._
import sbt.internal.util.ReadJsonFromInputStream
abstract class ServerConnection(connection: Socket) {
@@ -25,69 +28,29 @@ abstract class ServerConnection(connection: Socket) {
val thread = new Thread(s"sbt-serverconnection-${connection.getPort}") {
override def run(): Unit = {
try {
val readBuffer = new Array[Byte](4096)
val in = connection.getInputStream
connection.setSoTimeout(5000)
var buffer: Vector[Byte] = Vector.empty
def readFrame: Vector[Byte] = {
def getContentLength: Int = {
readLine.drop(16).toInt
}
val l = getContentLength
readLine
readLine
readContentLength(l)
}
def readLine: String = {
if (buffer.isEmpty) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
}
}
val delimPos = buffer.indexOf(delimiter)
if (delimPos > 0) {
val chunk0 = buffer.take(delimPos)
buffer = buffer.drop(delimPos + 1)
// remove \r at the end of line.
val chunk1 = if (chunk0.lastOption contains retByte) chunk0.dropRight(1) else chunk0
new String(chunk1.toArray, "utf-8")
} else readLine
}
def readContentLength(length: Int): Vector[Byte] = {
if (buffer.size < length) {
val bytesRead = in.read(readBuffer)
if (bytesRead > 0) {
buffer = buffer ++ readBuffer.toVector.take(bytesRead)
} else ()
} else ()
if (length <= buffer.size) {
val chunk = buffer.take(length)
buffer = buffer.drop(length)
chunk
} else readContentLength(length)
}
while (running.get) {
try {
val frame = readFrame
Serialization
.deserializeJsonMessage(frame)
.fold(
{ errorDesc =>
val s = frame.mkString("") // new String(: Array[Byte], "UTF-8")
println(s"Got invalid chunk from server: $s \n" + errorDesc)
},
_ match {
case msg: JsonRpcRequestMessage => onRequest(msg)
case msg: JsonRpcResponseMessage => onResponse(msg)
case msg: JsonRpcNotificationMessage => onNotification(msg)
}
)
val frame = ReadJsonFromInputStream(in, running, None)
if (running.get) {
Serialization
.deserializeJsonMessage(frame)
.fold(
{ errorDesc =>
val s = frame.mkString("") // new String(: Array[Byte], "UTF-8")
println(s"Got invalid chunk from server: $s \n" + errorDesc)
},
_ match {
case msg: JsonRpcRequestMessage => onRequest(msg)
case msg: JsonRpcResponseMessage => onResponse(msg)
case msg: JsonRpcNotificationMessage => onNotification(msg)
}
)
}
} catch {
case _: SocketTimeoutException => // its ok
case e: IOException => running.set(false)
}
}
} finally {
@@ -0,0 +1,84 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.util
import java.io.InputStream
import java.nio.channels.ClosedChannelException
import java.util.concurrent.atomic.AtomicBoolean
import scala.collection.mutable
import scala.util.Try
private[sbt] object ReadJsonFromInputStream {
def apply(
inputStream: InputStream,
running: AtomicBoolean,
onHeader: Option[String => Unit]
): Seq[Byte] = {
val newline = '\n'.toInt
val carriageReturn = '\r'.toInt
val contentLength = "Content-Length: "
var bytes = new mutable.ArrayBuffer[Byte]
def getLine(): String = {
val line = new String(bytes.toArray, "UTF-8")
bytes = new mutable.ArrayBuffer[Byte]
onHeader.foreach(oh => oh(line))
line
}
var content: Seq[Byte] = Seq.empty[Byte]
var consecutiveLineEndings = 0
var onCarriageReturn = false
do {
val byte = inputStream.read
byte match {
case `newline` =>
val line = getLine()
if (onCarriageReturn) consecutiveLineEndings += 1
onCarriageReturn = false
if (line.startsWith(contentLength)) {
Try(line.drop(contentLength.length).toInt) foreach { len =>
def drainHeaders(): Unit =
do {
inputStream.read match {
case `newline` if onCarriageReturn =>
getLine()
onCarriageReturn = false
consecutiveLineEndings += 1
case `carriageReturn` => onCarriageReturn = true
case c =>
if (c == newline) getLine()
else bytes += c.toByte
onCarriageReturn = false
consecutiveLineEndings = 0
}
} while (consecutiveLineEndings < 2)
drainHeaders()
val buf = new Array[Byte](len)
var offset = 0
do {
offset += inputStream.read(buf, offset, len - offset)
} while (offset < len)
content = buf.toSeq
}
} else if (line.startsWith("{")) {
// Assume this is a json object with no headers
content = line.getBytes.toSeq
}
case i if i < 0 =>
running.set(false)
throw new ClosedChannelException
case `carriageReturn` => onCarriageReturn = true
case c =>
onCarriageReturn = false
bytes += c.toByte
}
} while (content.isEmpty && running.get)
content
}
}