diff --git a/build.sbt b/build.sbt index 3e7aa0ad3..542ad398c 100644 --- a/build.sbt +++ b/build.sbt @@ -620,6 +620,7 @@ lazy val actionsProj = (project in file("main-actions")) // an internal (sbt.internal) type not meant for external consumption. exclude[DirectMissingMethodProblem]("sbt.internal.WorkerConnection.valueOf"), exclude[DirectMissingMethodProblem]("sbt.internal.WorkerConnection.values"), + exclude[DirectMissingMethodProblem]("sbt.internal.*"), ), ) .dependsOn(lmCore) diff --git a/internal/util-core/src/main/scala/sbt/internal/util/Util.scala b/internal/util-core/src/main/scala/sbt/internal/util/Util.scala index 12a897222..602af1ad7 100644 --- a/internal/util-core/src/main/scala/sbt/internal/util/Util.scala +++ b/internal/util-core/src/main/scala/sbt/internal/util/Util.scala @@ -13,7 +13,7 @@ import java.util.Locale import scala.collection.concurrent.TrieMap import scala.reflect.Selectable.reflectiveSelectable -import scala.util.Properties +import scala.util.{ Properties, Try } object Util: def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) @@ -50,6 +50,8 @@ object Util: () } + def ignoreTry[A](f: => A): Unit = ignoreResult(Try(f)) + lazy val isMac: Boolean = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("mac") diff --git a/lm-core/src/main/scala/sbt/internal/librarymanagement/LMSysProp.scala b/lm-core/src/main/scala/sbt/internal/librarymanagement/LMSysProp.scala index 209e0cd11..6eb5e4b96 100644 --- a/lm-core/src/main/scala/sbt/internal/librarymanagement/LMSysProp.scala +++ b/lm-core/src/main/scala/sbt/internal/librarymanagement/LMSysProp.scala @@ -9,10 +9,11 @@ package sbt package internal package librarymanagement +import java.io.File import java.util.Locale import scala.util.control.NonFatal -object LMSysProp { +object LMSysProp: def booleanOpt(name: String): Option[Boolean] = sys.props.get(name).flatMap { x => x.toLowerCase(Locale.ENGLISH) match { @@ -63,4 +64,12 @@ object LMSysProp { lazy val maxPublishAttempts: Int = java.lang.Integer.getInteger("sbt.repository.publish.attempts", 3) -} + /** + * Ivy home directory: `sbt.ivy.home`, then `ivy.home`, else `user.home/.ivy2`. + */ + lazy val defaultIvyHome: File = + def prop(name: String): Option[String] = sys.props.get(name).map(_.trim).filter(_.nonEmpty) + prop("sbt.ivy.home").orElse(prop("ivy.home")) match + case Some(home) => File(home) + case None => File(sbt.io.Path.userHome, ".ivy2") +end LMSysProp diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala index 1c91acdd4..2ee909b0f 100644 --- a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala +++ b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionParams.scala @@ -8,6 +8,7 @@ import coursier.core.* import coursier.params.rule.Strict import lmcoursier.FallbackDependency import lmcoursier.definitions.ToCoursier +import sbt.internal.librarymanagement.LMSysProp import coursier.util.Task import scala.collection.mutable @@ -114,7 +115,7 @@ object ResolutionParams { val ivyHome = sys.props .get("ivy.home") .orElse(ivyHomeOpt.map(_.getAbsoluteFile.toURI.getPath)) - .getOrElse(new File(sys.props("user.home")).toURI.getPath + ".ivy2") + .getOrElse(LMSysProp.defaultIvyHome.getAbsoluteFile.toURI.getPath) val sbtIvyHome = sys.props.getOrElse( "sbt.ivy.home", diff --git a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala index 04859ed5f..f6ed25f13 100644 --- a/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala +++ b/lm-coursier/src/main/scala/lmcoursier/internal/ResolutionRun.scala @@ -1,6 +1,7 @@ package lmcoursier.internal -import coursier.{ Resolution, Resolve } +import coursier.{ CoursierEnv, Resolution, Resolve } +import coursier.cache.CacheEnv import coursier.cache.internal.ThreadUtil import coursier.cache.loggers.{ FallbackRefreshDisplay, ProgressBarRefreshDisplay, RefreshLogger } import coursier.core.* @@ -8,8 +9,9 @@ import coursier.error.ResolutionError import coursier.error.ResolutionError.CantDownloadModule import coursier.ivy.IvyRepository import coursier.maven.MavenRepositoryLike +import coursier.params.Mirror import coursier.params.rule.RuleResolution -import coursier.util.Task +import coursier.util.{ EnvValues, Task } import sbt.util.Logger import scala.annotation.nowarn @@ -19,6 +21,35 @@ import scala.collection.mutable // private[coursier] object ResolutionRun { + /** + * Mirrors from the coursier configuration, plus the ones of Maven's settings.xml + * only when COURSIER_MAVEN_SETTINGS (or coursier.maven-settings) is set explicitly, + * since a settings.xml mirror of `*` would otherwise shadow the resolvers of the build. + * See https://github.com/sbt/sbt/issues/9821 + */ + lazy val defaultMirrors: Seq[Mirror] = + CoursierEnv.defaultMirrors( + CoursierEnv.mirrors.read(), + CoursierEnv.mirrorsExtra.read(), + CoursierEnv.scalaCliConfig.read(), + CacheEnv.configDir.read() + ) ++ + mavenSettingsMirrors( + CoursierEnv.mavenSettings.read(), + CoursierEnv.mavenHome.read(), + CoursierEnv.mavenHomeFallback.read() + ) + + private[lmcoursier] def mavenSettingsMirrors( + mavenSettings: EnvValues, + mavenHome: EnvValues, + mavenHomeFallback: EnvValues + ): Seq[Mirror] = + val explicit = mavenSettings.env.orElse(mavenSettings.prop).exists(_.trim.nonEmpty) + if explicit then + CoursierEnv.defaultMavenSettingsMirrors(mavenSettings, mavenHome, mavenHomeFallback) + else Nil + private def resolution( params: ResolutionParams, verbosityLevel: Int, @@ -103,6 +134,7 @@ object ResolutionRun { }, boms = params.boms, repositories = repositories, + mirrors = defaultMirrors, resolutionParams = params.params .addForceVersion( (if (isSandboxConfig) Nil diff --git a/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala b/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala index a3ad3f4e0..64d4b9f03 100644 --- a/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala +++ b/lm-coursier/src/test/scala/lmcoursier/internal/ResolutionRunSpec.scala @@ -1,11 +1,16 @@ package lmcoursier.internal +import coursier.CoursierEnv import coursier.core.{ Module, ModuleName, Organization, Resolution } import coursier.error.ResolutionError.CantDownloadModule +import coursier.util.EnvValues import coursier.version.VersionConstraint +import sbt.io.IO import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import java.io.File + class ResolutionRunSpec extends AnyFunSuite with Matchers: private def cantDownload(errors: String*): CantDownloadModule = @@ -37,3 +42,48 @@ class ResolutionRunSpec extends AnyFunSuite with Matchers: "Server returned HTTP response code: 404 for URL: https://repo.example.com/org/mod/1.0/mod-1.0.pom" ) ResolutionRun.isTransientResolutionError(err) shouldBe false + + private val unset = EnvValues(None, None) + + private def withMirrorOfAll(f: File => Unit): Unit = + IO.withTemporaryDirectory: dir => + IO.write( + new File(dir, "settings.xml"), + """ + | + | + | internal + | https://nexus.example.com/repository/maven-public + | * + | + | + | + |""".stripMargin + ) + f(dir) + + test("settings.xml mirrors are ignored unless the Maven settings are set explicitly"): + withMirrorOfAll: dir => + val mavenHome = EnvValues(Some(dir.getAbsolutePath), None) + assert(CoursierEnv.defaultMavenSettingsMirrors(unset, mavenHome, unset).nonEmpty) + assert(ResolutionRun.mavenSettingsMirrors(unset, mavenHome, unset).isEmpty) + + test("settings.xml mirrors are read when the Maven settings are set explicitly"): + withMirrorOfAll: dir => + val settings = new File(dir, "settings.xml").getAbsolutePath + assert( + ResolutionRun.mavenSettingsMirrors(EnvValues(Some(settings), None), unset, unset).size == 1 + ) + assert( + ResolutionRun.mavenSettingsMirrors(EnvValues(None, Some(settings)), unset, unset).size == 1 + ) + val mavenHome = EnvValues(Some(dir.getAbsolutePath), None) + assert( + ResolutionRun + .mavenSettingsMirrors(EnvValues(Some("true"), None), mavenHome, unset) + .size == 1 + ) + assert( + ResolutionRun.mavenSettingsMirrors(EnvValues(Some("false"), None), mavenHome, unset).isEmpty + ) +end ResolutionRunSpec diff --git a/main-actions/src/main/scala/sbt/internal/WorkerExchange.scala b/main-actions/src/main/scala/sbt/internal/WorkerExchange.scala index fe22ca67c..eae02d416 100644 --- a/main-actions/src/main/scala/sbt/internal/WorkerExchange.scala +++ b/main-actions/src/main/scala/sbt/internal/WorkerExchange.scala @@ -24,8 +24,9 @@ import scala.sys.process.{ BasicIO, Process, ProcessIO } import scala.collection.mutable import scala.collection.concurrent.TrieMap import scala.collection.mutable.ListBuffer -import scala.concurrent.{ Await, Promise } +import scala.concurrent.{ Await, Future, Promise } import scala.concurrent.duration.* +import scala.util.Try import scala.util.control.NonFatal object WorkerExchange: @@ -83,11 +84,13 @@ object WorkerExchange: IO.classLocationPath(classOf[Gson]).toFile, ) val inputRef = Promise[OutputStream]() + val responsesRead = Promise[Unit]() def runAccepter(out: OutputStream, in: InputStream): Unit = inputRef.success(out) val scanner = Scanner(in, "UTF-8") while scanner.hasNextLine() do notifyListeners(scanner.nextLine()) - val (connArgs, closer): (Seq[String], Option[AutoCloseable]) = connectionType match + responsesRead.trySuccess(()) + val (connArgs, closer) = connectionType match case WorkerConnection.Tcp => val serverSocket = Retry(ServerSocket(0, 1, loopback)) val accepter = Thread(() => { @@ -130,20 +133,24 @@ object WorkerExchange: val onStdoutLine: String => Unit = connectionType match case WorkerConnection.Stdio => notifyListeners case _ => (line) => scala.Console.out.println(line) + def readStdout(stdout: InputStream): Unit = + BasicIO.processFully(onStdoutLine)(stdout) + if connectionType == WorkerConnection.Stdio then responsesRead.trySuccess(()) val processIo = ProcessIO( in = (input) => (connectionType match case WorkerConnection.Stdio => inputRef.success(input) case _ => () ), - out = BasicIO.processFully(onStdoutLine), + out = readStdout, err = BasicIO.processFully((line) => scala.Console.err.println(line)), ) val forkWithIo = fo.withOutputStrategy(OutputStrategy.CustomInputOutput(processIo)) val p = Fork.java.fork(forkWithIo, options) val forkTimeout = fo.connectionTimeout.getOrElse(30.seconds) val input = Await.result(inputRef.future, forkTimeout) - WorkerProxy(input, p, options, closer) + WorkerProxy(input, p, options, closer, responsesRead.future) + end startWorker /** Generates a fresh path suitable for binding a `WorkerConnection.Ipc` socket. */ def newIpcSocketPath(): NioPath = @@ -178,6 +185,7 @@ class WorkerProxy( val process: Process, val options: Seq[String], closer: Option[AutoCloseable], + responsesRead: Future[Unit], ) extends AutoCloseable: lazy val inputStream = PrintStream(input) def close(): Unit = @@ -194,11 +202,21 @@ class WorkerProxy( val watch = Thread(() => { while process.isAlive() do Thread.sleep(100) + Try(Await.ready(responsesRead, WorkerProxy.responseDrainTimeout)) WorkerExchange.listeners.foreach(_.notifyExit(process)) }) watch.start() end WorkerProxy +object WorkerProxy: + /** + * How long to keep reading a worker's responses after it exits, before reporting the exit. + * The reader reaches end of stream as soon as it has consumed everything the worker wrote; + * the bound only matters for a worker that died before connecting. + */ + private val responseDrainTimeout = 10.seconds +end WorkerProxy + abstract class WorkerResponseListener extends Function1[String, Unit]: def notifyExit(p: Process): Unit diff --git a/main-command/src/main/scala/sbt/internal/AtomicCloseable.scala b/main-command/src/main/scala/sbt/internal/AtomicCloseable.scala new file mode 100644 index 000000000..f4dc92fa3 --- /dev/null +++ b/main-command/src/main/scala/sbt/internal/AtomicCloseable.scala @@ -0,0 +1,47 @@ +/* + * 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 +package internal + +import java.util.concurrent.atomic.AtomicReference + +import sbt.internal.util.Util + +/** Holds a closeable that a caller replaces, and closes the one it replaces. */ +private[sbt] class AtomicCloseable[A >: Null <: AutoCloseable](val ref: AtomicReference[A]) + extends AnyVal: + def get: A = ref.get + def set(c: A): Unit = AtomicCloseable.close(ref.getAndSet(c)) + def close(): Unit = AtomicCloseable.close(AtomicCloseable.release(ref)) + + /** Keeps the value another caller put here, and closes the one this caller built. */ + def setIfEmpty(ctor: => A): A = + var obj = ref.get + if obj eq null then + val value = ctor + require(value ne null, "AtomicCloseable.setIfEmpty: `ctor` must not return null") + while obj eq null do obj = if ref.compareAndSet(null, value) then value else ref.get + if obj ne value then AtomicCloseable.close(value) + obj +end AtomicCloseable + +private[sbt] object AtomicCloseable: + def apply[A >: Null <: AutoCloseable](): AtomicCloseable[A] = + new AtomicCloseable(new AtomicReference[A]) + + def apply[A >: Null <: AutoCloseable](c: A): AtomicCloseable[A] = + new AtomicCloseable(new AtomicReference[A](c)) + + def close(obj: AutoCloseable): Unit = + if obj ne null then Util.ignoreTry(obj.close()) + + def release[A >: Null <: AutoCloseable](ref: AtomicReference[A]): A = + ref.getAndSet(null) + +end AtomicCloseable diff --git a/main-command/src/main/scala/sbt/internal/server/Server.scala b/main-command/src/main/scala/sbt/internal/server/Server.scala index e71d6e643..692399aa8 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -46,7 +46,7 @@ private[sbt] object Server { def start( connection: ServerConnection, - onIncomingSocket: (Socket, ServerInstance) => Unit, + onIncomingSocket: (AtomicReference[Socket], ServerInstance) => Unit, log: Logger ): ServerInstance = new ServerInstance { self => @@ -109,14 +109,19 @@ private[sbt] object Server { running.set(true) p.success(()) while (running.get()) { + val clientSocket = AtomicCloseable[Socket]() try { - val socket = serverSocket.accept() - onIncomingSocket(socket, self) + clientSocket.set(serverSocket.accept()) + onIncomingSocket(clientSocket.ref, self) } catch { + case scala.util.control.NonFatal(e) if clientSocket.get ne null => + log.error(s"sbt server failed to serve a client: $e") + log.trace(e) case e: IOException if Option(e.getMessage).exists(_.contains("connect")) => case _: SocketTimeoutException => // its ok case _: SocketException if !running.get => // the server is shutting down } + clientSocket.close() } serverSocketHolder.get match { case null => diff --git a/main-command/src/test/scala/sbt/internal/AtomicCloseableSpec.scala b/main-command/src/test/scala/sbt/internal/AtomicCloseableSpec.scala new file mode 100644 index 000000000..d26f1096f --- /dev/null +++ b/main-command/src/test/scala/sbt/internal/AtomicCloseableSpec.scala @@ -0,0 +1,72 @@ +/* + * 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 +package internal + +import verify.BasicTestSuite + +object AtomicCloseableSpec extends BasicTestSuite: + class Probe extends AutoCloseable: + var closed: Boolean = false + override def close(): Unit = closed = true + end Probe + + test("a holder built around a value"): + val probe = new Probe + val holder = AtomicCloseable(probe) + assert(holder.get == probe) + assert(!probe.closed) + holder.close() + assert(probe.closed) + + test("a value that replaces another closes it"): + val holder = AtomicCloseable[Probe]() + val first, second = new Probe + holder.set(first) + holder.set(second) + assert(first.closed) + assert(!second.closed) + assert(holder.get == second) + + test("closing empties the holder"): + val holder = AtomicCloseable[Probe]() + val probe = new Probe + holder.set(probe) + holder.close() + assert(probe.closed) + assert(holder.get == null) + + test("closing an empty holder does nothing"): + AtomicCloseable[Probe]().close() + + test("a holder that has a value keeps it"): + val holder = AtomicCloseable[Probe]() + val first = new Probe + holder.set(first) + val second = new Probe + assert(holder.setIfEmpty(second) == first) + assert(!first.closed) + assert(!second.closed) + + test("a holder that has no value takes the one it is given"): + val holder = AtomicCloseable[Probe]() + val probe = new Probe + assert(holder.setIfEmpty(probe) == probe) + assert(holder.get == probe) + assert(!probe.closed) + + test("a value that loses a race is closed"): + val holder = AtomicCloseable[Probe]() + val winner, loser = new Probe + // the holder fills while this caller builds its own value + val result = holder.setIfEmpty { holder.set(winner); loser } + assert(result == winner) + assert(loser.closed) + assert(!winner.closed) +end AtomicCloseableSpec diff --git a/main/src/main/scala/sbt/coursierint/LMCoursier.scala b/main/src/main/scala/sbt/coursierint/LMCoursier.scala index 70233ac7e..faa3c2664 100644 --- a/main/src/main/scala/sbt/coursierint/LMCoursier.scala +++ b/main/src/main/scala/sbt/coursierint/LMCoursier.scala @@ -61,7 +61,7 @@ object LMCoursier { sys.env .get("LOCALAPPDATA") .map(absoluteFile) - .getOrElse(absoluteFile(sys.props("user.home")) / "AppData" / "Local") + .getOrElse(sbt.io.Path.userHome.getAbsoluteFile / "AppData" / "Local") base / "Coursier" / "Cache" / "v1" } sys.props diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 889c358ca..36fd5339e 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -205,19 +205,21 @@ private[sbt] final class CommandExchange { lazy val enableBsp = s.get(bspEnabled).getOrElse(true) lazy val portfile = s.baseDir / "project" / "target" / "active.json" - def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = { + def onIncomingSocket(socket: AtomicReference[Socket], instance: ServerInstance): Unit = { val name = newNetworkName Terminal.consoleLog(s"new client connected: $name") val channel = new NetworkChannel( name, - socket, + socket.get, auth, instance, handlers, mkAskUser(name), ) subscribe(channel) + channel.start() + AtomicCloseable.release(socket) // i took over } if (server.isEmpty && firstInstance.get) { val h = Hash.halfHashString(IO.toURI(portfile).toString) diff --git a/main/src/main/scala/sbt/internal/SysProp.scala b/main/src/main/scala/sbt/internal/SysProp.scala index 41369c245..ae90de146 100644 --- a/main/src/main/scala/sbt/internal/SysProp.scala +++ b/main/src/main/scala/sbt/internal/SysProp.scala @@ -202,7 +202,7 @@ object SysProp: def secondaryIdleTimeoutSec: Long = long("sbt.server.secondaryIdleTimeout", 600L) private def file(value: String): File = new File(value) - private def home: File = file(sys.props("user.home")) + private def home: File = sbt.io.Path.userHome /** * Default directory for global sbt config (plugins, settings). Respects XDG Base Directory diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 05809670b..341848eeb 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -965,8 +965,10 @@ final class NetworkChannel( private[sbt] def isAttached: Boolean = attached.get private[sbt] def isInitialized: Boolean = initialized - thread.start() - writeThread.start() + private[sbt] def start(): Unit = + thread.start() + writeThread.start() + } object NetworkChannel { diff --git a/main/src/test/scala/sbt/internal/server/NetworkChannelSpec.scala b/main/src/test/scala/sbt/internal/server/NetworkChannelSpec.scala index 83a043243..7443d40b8 100644 --- a/main/src/test/scala/sbt/internal/server/NetworkChannelSpec.scala +++ b/main/src/test/scala/sbt/internal/server/NetworkChannelSpec.scala @@ -13,7 +13,10 @@ import java.net.{ InetAddress, ServerSocket, Socket } import sbt.{ State, StandardMain } import sbt.internal.util.{ AttributeMap, ConsoleOut, GlobalLogging, MainAppender, Terminal, Util } import sbt.internal.util.Terminal.TerminalImpl +import sbt.internal.protocol.JsonRpcRequestMessage import sbt.protocol.Serialization +import sjsonnew.support.scalajson.unsafe.Parser +import scala.concurrent.duration.* import scala.jdk.CollectionConverters.* import scala.util.Using import verify.BasicTestSuite @@ -78,6 +81,31 @@ object NetworkChannelSpec extends BasicTestSuite: val outcome = whileInterrupted(StandardMain.exchange.removeChannel(channel)) assertSucceededAndStillInterrupted(outcome) + test("an attach sent before the channel is subscribed still attaches the client"): + withServerState: + withChannelThreadsJoined: + withLoopbackConnection: (client, connection) => + client.getOutputStream.write(attachRequest) + client.getOutputStream.flush() + Using.resource(newChannel(connection, Seq(VirtualTerminal.handler))): channel => + Thread.sleep(acceptThreadDelay.toMillis) + StandardMain.exchange.subscribe(channel) + channel.start() + assert(eventually(channel.isAttached), "the attach request was dropped") + + private val acceptThreadDelay = 200.millis + + private def attachRequest: Array[Byte] = + val params = Parser.parseUnsafe("""{"interactive": false}""") + Serialization.serializeRequestMessage( + JsonRpcRequestMessage("2.0", "attach-id", Serialization.attach, Some(params)) + ) + + private def eventually(condition: => Boolean): Boolean = + val deadline = 5.seconds.fromNow + while !condition && deadline.hasTimeLeft() do Thread.sleep(10) + condition + private type Outcome = (thrown: Option[Exception], stillInterrupted: Boolean) private given Using.Releasable[NetworkChannel] = _.shutdown(false) @@ -104,7 +132,7 @@ object NetworkChannelSpec extends BasicTestSuite: private def withAttachedClient[A](test: NetworkChannel => A): A = withServerState: withChannelThreadsJoined: - withLoopbackConnection: connection => + withLoopbackConnection: (_, connection) => Using.resource(attachedChannel(connection))(test) private def withChannelThreadsJoined[A](f: => A): A = @@ -120,26 +148,30 @@ object NetworkChannelSpec extends BasicTestSuite: thread.getName.startsWith("sbt-networkchannel-") || thread.getName.startsWith(s"sbt-$channelName-") - private def attachedChannel(connection: Socket): NetworkChannel = - val channel = new NetworkChannel( + private def newChannel(connection: Socket, handlers: Seq[ServerHandler]): NetworkChannel = + new NetworkChannel( name = channelName, connection = connection, auth = Set.empty, instance = null, - handlers = Nil, + handlers = handlers, mkUIThreadImpl = (_, _) => null, ) + + private def attachedChannel(connection: Socket): NetworkChannel = + val channel = newChannel(connection, Nil) + channel.start() val attachRequestId = "attached-id" channel.setInteractive(attachRequestId, value = false) channel - private def withLoopbackConnection[A](f: Socket => A): A = + private def withLoopbackConnection[A](f: (Socket, Socket) => A): A = val loopback = InetAddress.getLoopbackAddress val anyFreePort = 0 val backlog = 1 Using.resource(new ServerSocket(anyFreePort, backlog, loopback)): server => - Using.resource(new Socket(loopback, server.getLocalPort)): _ => - Using.resource(server.accept())(f) + Using.resource(new Socket(loopback, server.getLocalPort)): client => + Using.resource(server.accept())(f(client, _)) private def withServerState[A](f: => A): A = val previous = StandardMain.exchange.withState(Option(_)) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index de79d1712..5e3108724 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -12,7 +12,7 @@ object Dependencies { sys.env.get("BUILD_VERSION") orElse sys.props.get("sbt.build.version") // sbt modules - val ioVersion = nightlyVersion.getOrElse("1.13.4") + val ioVersion = nightlyVersion.getOrElse("1.13.5") val zincVersion = nightlyVersion.getOrElse("2.0.4") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion diff --git a/sbt-app/src/sbt-test/tests/fork-slow-listener/build.sbt b/sbt-app/src/sbt-test/tests/fork-slow-listener/build.sbt new file mode 100644 index 000000000..9f889b515 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/fork-slow-listener/build.sbt @@ -0,0 +1,11 @@ +scalaVersion := "3.9.0" +Test / fork := true +libraryDependencies += "org.scalameta" %% "munit" % "1.0.4" % Test + +Test / testListeners += new TestsListener: + def doInit(): Unit = () + def startGroup(name: String): Unit = () + def testEvent(event: TestEvent): Unit = Thread.sleep(50) + def endGroup(name: String, t: Throwable): Unit = () + def endGroup(name: String, result: TestResult): Unit = () + def doComplete(finalResult: TestResult): Unit = () diff --git a/sbt-app/src/sbt-test/tests/fork-slow-listener/src/test/scala/ManyTests.scala b/sbt-app/src/sbt-test/tests/fork-slow-listener/src/test/scala/ManyTests.scala new file mode 100644 index 000000000..9f1ae0f39 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/fork-slow-listener/src/test/scala/ManyTests.scala @@ -0,0 +1,2 @@ +class ManyTests extends munit.FunSuite: + (1 to 40).foreach(i => test(s"test $i")(assert(i < 40))) diff --git a/sbt-app/src/sbt-test/tests/fork-slow-listener/test b/sbt-app/src/sbt-test/tests/fork-slow-listener/test new file mode 100644 index 000000000..5a9f22365 --- /dev/null +++ b/sbt-app/src/sbt-test/tests/fork-slow-listener/test @@ -0,0 +1 @@ +-> test diff --git a/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala b/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala index 9ea5d12e8..26d83f852 100644 --- a/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala +++ b/zinc-lm-integration/src/main/scala/sbt/internal/inc/ZincComponentCompiler.scala @@ -11,7 +11,7 @@ package internal package inc import sbt.internal.inc.classpath.ClasspathUtil -import sbt.internal.librarymanagement.JsonUtil +import sbt.internal.librarymanagement.{ JsonUtil, LMSysProp } import sbt.internal.util.{ BufferedLogger, FullLogger } import sbt.io.IO import sbt.librarymanagement.* @@ -171,12 +171,7 @@ private[sbt] object ZincComponentCompiler { new ZincCompilerBridgeProvider(None, manager, dependencyResolution, scalaJarsTarget) private final val LocalIvy = - (sys.props.get("sbt.ivy.home") match { - case Some(home) => - if (home.endsWith("/")) home - else home + "/" - case _ => s"$${user.home}/.ivy2/" - }) + "local/" + Resolver.localBasePattern + Resolver.resolvePattern(LMSysProp.defaultIvyHome.toString, "local/" + Resolver.localBasePattern) final val LocalResolver: Resolver = { val toUse = Vector(LocalIvy)