[2.x] fix: sbtn to retry on corrupt active.json (#9617)

**Problem**
sbtn gets stuck when active.json is corrupt.

**Solution**
Delete active.json, and retry.
This commit is contained in:
eugene yokota 2026-08-19 02:26:44 -04:00 committed by GitHub
parent 6b3d7c6301
commit 8753a98161
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 67 additions and 20 deletions

View File

@ -196,7 +196,7 @@ class NetworkClient(
promptCompleteUsers: Boolean,
retry: Boolean
): (Socket, Option[String]) =
try {
try
if (!portfile.exists) {
if (shutdownOnly) {
console.appendLog(Level.Info, "no sbt server is running. ciao")
@ -246,12 +246,14 @@ class NetworkClient(
}
}
connect(0)
} catch {
case e: ConnectionRefusedException if retry =>
if (Files.deleteIfExists(portfile.toPath))
catch
case e @ (_: ConnectionRefusedException | _: ClientSocket.ConnectionFileReadException)
if retry =>
errorStream.println(s"${e.getMessage}; starting a new server")
if Files.deleteIfExists(portfile.toPath) then
connectOrStartServerAndConnect(promptCompleteUsers, retry = false)
else throw e
}
end connectOrStartServerAndConnect
// Open server connection based on the portfile
def init(promptCompleteUsers: Boolean, retry: Boolean): ServerSession = {

View File

@ -69,7 +69,7 @@ object NetworkClientSilentDisconnectTest extends BasicTestSuite:
s"client exited 1 without explaining the disconnect: '$explained'"
)
test("a corrupt portfile must not fail silently"):
test("a corrupt portfile is replaced and a fresh server connection is attempted"):
val base = Files.createTempDirectory("corrupt-portfile-project").toFile
Files.writeString(
base.toPath.resolve("build.sbt"),
@ -77,22 +77,58 @@ object NetworkClientSilentDisconnectTest extends BasicTestSuite:
)
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'"
)
val portfile = target.resolve("active.json")
Files.writeString(portfile, "not json")
try
val (_, explained) = runBatchClient(base)
assert(
explained.contains("corrupt or unreadable") &&
explained.contains("active.json") &&
explained.contains("starting a new server"),
s"client did not explain the corrupt portfile before retrying: '$explained'"
)
assert(
!Files.exists(portfile) || Files.readString(portfile) != "not json",
"corrupt portfile should have been replaced by a fresh server connection"
)
finally shutdownServer(base)
private def runBatchClient(base: File): (Int, String) =
test("an empty portfile is replaced and a fresh server connection is attempted"):
val base = Files.createTempDirectory("empty-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)
val portfile = target.resolve("active.json")
Files.writeString(portfile, "")
try
val (_, explained) = runBatchClient(base)
assert(
explained.contains("corrupt or unreadable") &&
explained.contains("active.json") &&
explained.contains("starting a new server"),
s"client did not explain the corrupt portfile before retrying: '$explained'"
)
assert(
!Files.exists(portfile) || Files.readString(portfile).nonEmpty,
"empty portfile should have been replaced by a fresh server connection"
)
finally shutdownServer(base)
private def shutdownServer(base: File): Unit =
try runBatchClient(base, Array("shutdown"))
catch case _: Exception => ()
private def runBatchClient(base: File, args: Array[String] = Array("compile")): (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"),
args,
new InputStream { override def read(): Int = -1 },
out,
err,

View File

@ -12,6 +12,7 @@ package protocol
import java.io.{ File, InputStream, OutputStream }
import java.net.{ InetAddress, Socket, StandardProtocolFamily, URI, UnixDomainSocketAddress }
import java.nio.channels.{ Channels, SocketChannel }
import scala.util.control.NonFatal
import sjsonnew.BasicJsonProtocol
import sjsonnew.support.scalajson.unsafe.{ Parser, Converter }
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
@ -23,18 +24,26 @@ import org.scalasbt.ipcsocket.*
object ClientSocket {
private lazy val fileFormats = new BasicJsonProtocol with PortFileFormats with TokenFileFormats {}
/** Thrown when a server connection file can't be read or parsed as JSON. */
final class ConnectionFileReadException(file: File, cause: Throwable)
extends Exception(s"sbt connection file $file is corrupt or unreadable: $cause", cause)
def socket(portfile: File): (Socket, Option[String]) = socket(portfile, false)
def socket(portfile: File, useJNI: Boolean): (Socket, Option[String]) = {
import fileFormats.given
val json: JValue = Parser.parseFromString(sbt.io.IO.read(portfile)).get
val p = Converter.fromJson[PortFile](json).get
val p =
try
val json: JValue = Parser.parseFromString(sbt.io.IO.read(portfile)).get
Converter.fromJson[PortFile](json).get
catch case NonFatal(e) => throw new ConnectionFileReadException(portfile, e)
val uri = new URI(p.uri)
// println(uri)
val token = p.tokenfilePath map { tp =>
val tokeFile = new File(tp)
val json: JValue = Parser.parseFromFile(tokeFile).get
val t = Converter.fromJson[TokenFile](json).get
t.token
try
val json: JValue = Parser.parseFromFile(tokeFile).get
Converter.fromJson[TokenFile](json).get.token
catch case NonFatal(e) => throw new ConnectionFileReadException(tokeFile, e)
}
val sk = uri.getScheme match {
case "local" => localSocket(uri.getSchemeSpecificPart, useJNI)