mirror of https://github.com/sbt/sbt.git
[2.x] fix: sbtn to log connection errors
onClose now logs sbt server disconnected when the close wasn't initiated by the client. Generated-by: Oh My Pi (kimi-code/k3)
This commit is contained in:
parent
c0ffc9dc60
commit
e8f40d68c3
|
|
@ -156,6 +156,11 @@ class NetworkClient(
|
||||||
|
|
||||||
private def mkSocket(file: File): (Socket, Option[String]) = ClientSocket.socket(file, useJNI)
|
private def mkSocket(file: File): (Socket, Option[String]) = ClientSocket.socket(file, useJNI)
|
||||||
|
|
||||||
|
private[sbt] def logFailure(e: Exception): Unit = {
|
||||||
|
errorStream.println(s"sbt client failed: $e")
|
||||||
|
e.printStackTrace(errorStream)
|
||||||
|
}
|
||||||
|
|
||||||
private def portfile = arguments.baseDirectory / "project" / "target" / "active.json"
|
private def portfile = arguments.baseDirectory / "project" / "target" / "active.json"
|
||||||
|
|
||||||
def connection: ServerSession = connectionHolder.synchronized {
|
def connection: ServerSession = connectionHolder.synchronized {
|
||||||
|
|
@ -311,7 +316,12 @@ class NetworkClient(
|
||||||
override protected def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg)
|
override protected def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg)
|
||||||
override protected def onResponse(msg: JsonRpcResponseMessage): Unit = self.onResponse(msg)
|
override protected def onResponse(msg: JsonRpcResponseMessage): Unit = self.onResponse(msg)
|
||||||
override protected def onClose(): Unit = if (!rebooting.get) {
|
override protected def onClose(): Unit = if (!rebooting.get) {
|
||||||
if (exitClean.get != false) exitClean.set(!running.get)
|
if (exitClean.get != false) {
|
||||||
|
val serverDropped = running.get
|
||||||
|
exitClean.set(!serverDropped)
|
||||||
|
if (serverDropped && !shutdownOnly)
|
||||||
|
console.appendLog(Level.Error, "sbt server disconnected")
|
||||||
|
}
|
||||||
running.set(false)
|
running.set(false)
|
||||||
Option(interactiveThread.get).foreach(_.interrupt())
|
Option(interactiveThread.get).foreach(_.interrupt())
|
||||||
}
|
}
|
||||||
|
|
@ -1473,8 +1483,11 @@ object NetworkClient {
|
||||||
try {
|
try {
|
||||||
if (client.connect(promptCompleteUsers = false)) client.run()
|
if (client.connect(promptCompleteUsers = false)) client.run()
|
||||||
else 1
|
else 1
|
||||||
} catch { case _: Exception => 1 }
|
} catch {
|
||||||
finally client.close()
|
case e: Exception =>
|
||||||
|
client.logFailure(e)
|
||||||
|
1
|
||||||
|
} finally client.close()
|
||||||
}
|
}
|
||||||
def client(
|
def client(
|
||||||
baseDirectory: File,
|
baseDirectory: File,
|
||||||
|
|
@ -1505,8 +1518,11 @@ object NetworkClient {
|
||||||
if (client.connect(promptCompleteUsers = false)) client.run()
|
if (client.connect(promptCompleteUsers = false)) client.run()
|
||||||
else 1
|
else 1
|
||||||
}
|
}
|
||||||
} catch { case _: Exception => 1 }
|
} catch {
|
||||||
finally client.close()
|
case e: Exception =>
|
||||||
|
client.logFailure(e)
|
||||||
|
1
|
||||||
|
} finally client.close()
|
||||||
}
|
}
|
||||||
def client(
|
def client(
|
||||||
baseDirectory: File,
|
baseDirectory: File,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* sbt
|
||||||
|
* Copyright 2023, Scala center
|
||||||
|
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||||
|
* Copyright 2008 - 2010, Mark Harrah
|
||||||
|
* Licensed under Apache License 2.0 (see LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package sbt.internal.client
|
||||||
|
|
||||||
|
import java.io.{ ByteArrayOutputStream, File, InputStream, PrintStream }
|
||||||
|
import java.net.{ ServerSocket, Socket }
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import verify.BasicTestSuite
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regression test for the silent half of #9484: the server is up (its portfile
|
||||||
|
* exists and accepts connections) but drops the client's connection during a
|
||||||
|
* batch run. The client must say why it failed instead of exiting 1 silently.
|
||||||
|
*/
|
||||||
|
object NetworkClientSilentDisconnectTest extends BasicTestSuite:
|
||||||
|
|
||||||
|
/** A server that accepts one connection, reads nothing, then closes it. */
|
||||||
|
class DropServer:
|
||||||
|
val serverSocket = new ServerSocket(0, 50, java.net.InetAddress.getLoopbackAddress)
|
||||||
|
val accepted = new CountDownLatch(1)
|
||||||
|
val thread = new Thread(() =>
|
||||||
|
try
|
||||||
|
val s: Socket = serverSocket.accept()
|
||||||
|
accepted.countDown()
|
||||||
|
Thread.sleep(200) // let the client write its handshake into the kernel buffer
|
||||||
|
s.close()
|
||||||
|
catch case _: Exception => ()
|
||||||
|
)
|
||||||
|
thread.setDaemon(true)
|
||||||
|
thread.start()
|
||||||
|
def port: Int = serverSocket.getLocalPort
|
||||||
|
def close(): Unit =
|
||||||
|
try serverSocket.close()
|
||||||
|
catch case _: Exception => ()
|
||||||
|
|
||||||
|
def withDropServer[A](f: DropServer => A): A =
|
||||||
|
val server = new DropServer
|
||||||
|
try f(server)
|
||||||
|
finally server.close()
|
||||||
|
|
||||||
|
def projectWithPortfile(port: Int): File =
|
||||||
|
val base = Files.createTempDirectory("silent-disconnect-project").toFile
|
||||||
|
Files.writeString(
|
||||||
|
base.toPath.resolve("build.sbt"),
|
||||||
|
"lazy val root = (project in file(\".\"))\n"
|
||||||
|
)
|
||||||
|
val target = base.toPath.resolve("project").resolve("target")
|
||||||
|
Files.createDirectories(target)
|
||||||
|
Files.writeString(
|
||||||
|
target.resolve("active.json"),
|
||||||
|
s"""{"uri":"tcp://127.0.0.1:$port","tokenfilePath":null,"tokenfileUri":null}"""
|
||||||
|
)
|
||||||
|
base
|
||||||
|
|
||||||
|
test("a dropped connection during a batch run must not exit silently"):
|
||||||
|
withDropServer: server =>
|
||||||
|
val base = projectWithPortfile(server.port)
|
||||||
|
val (code, explained) = runBatchClient(base)
|
||||||
|
assert(code == 1, s"expected exit 1, got $code")
|
||||||
|
assert(
|
||||||
|
explained.contains("disconnected"),
|
||||||
|
s"client exited 1 without explaining the disconnect: '$explained'"
|
||||||
|
)
|
||||||
|
|
||||||
|
test("a corrupt portfile must not fail silently"):
|
||||||
|
val base = Files.createTempDirectory("corrupt-portfile-project").toFile
|
||||||
|
Files.writeString(
|
||||||
|
base.toPath.resolve("build.sbt"),
|
||||||
|
"lazy val root = (project in file(\".\"))\n"
|
||||||
|
)
|
||||||
|
val target = base.toPath.resolve("project").resolve("target")
|
||||||
|
Files.createDirectories(target)
|
||||||
|
Files.writeString(target.resolve("active.json"), "not json")
|
||||||
|
val (code, explained) = runBatchClient(base)
|
||||||
|
assert(code == 1, s"expected exit 1, got $code")
|
||||||
|
assert(
|
||||||
|
explained.contains("sbt client failed"),
|
||||||
|
s"client exited 1 without explaining the failure: '$explained'"
|
||||||
|
)
|
||||||
|
|
||||||
|
private def runBatchClient(base: File): (Int, String) =
|
||||||
|
val errBytes = new ByteArrayOutputStream
|
||||||
|
val outBytes = new ByteArrayOutputStream
|
||||||
|
val err = new PrintStream(errBytes, true)
|
||||||
|
val out = new PrintStream(outBytes, true)
|
||||||
|
val code = NetworkClient.client(
|
||||||
|
base,
|
||||||
|
Array("compile"),
|
||||||
|
new InputStream { override def read(): Int = -1 },
|
||||||
|
out,
|
||||||
|
err,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
(code, errBytes.toString("UTF-8") + outBytes.toString("UTF-8"))
|
||||||
|
end NetworkClientSilentDisconnectTest
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Don't exit silently when the thin client loses its connection to the server mid-run or hits an exception during connect. The client now logs `sbt server disconnected` when the server drops the connection, and prints the exception with a stack trace when the connect or run phase throws. Previously these paths returned exit code 1 with no output, which made CI failures undebuggable. Addresses [#9484](https://github.com/sbt/sbt/issues/9484).
|
||||||
Loading…
Reference in New Issue