[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:
BrianHotopp 2026-08-17 14:08:21 -04:00 committed by GitHub
parent c0ffc9dc60
commit e8f40d68c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 126 additions and 5 deletions

View File

@ -156,6 +156,11 @@ class NetworkClient(
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"
def connection: ServerSession = connectionHolder.synchronized {
@ -311,7 +316,12 @@ class NetworkClient(
override protected def onRequest(msg: JsonRpcRequestMessage): Unit = self.onRequest(msg)
override protected def onResponse(msg: JsonRpcResponseMessage): Unit = self.onResponse(msg)
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)
Option(interactiveThread.get).foreach(_.interrupt())
}
@ -1473,8 +1483,11 @@ object NetworkClient {
try {
if (client.connect(promptCompleteUsers = false)) client.run()
else 1
} catch { case _: Exception => 1 }
finally client.close()
} catch {
case e: Exception =>
client.logFailure(e)
1
} finally client.close()
}
def client(
baseDirectory: File,
@ -1505,8 +1518,11 @@ object NetworkClient {
if (client.connect(promptCompleteUsers = false)) client.run()
else 1
}
} catch { case _: Exception => 1 }
finally client.close()
} catch {
case e: Exception =>
client.logFailure(e)
1
} finally client.close()
}
def client(
baseDirectory: File,

View 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

View File

@ -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).