From a4984c7042e2cb079bf18e25835e9a7cb657a60b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 10 Jul 2026 22:17:11 -0400 Subject: [PATCH 1/5] [2.x] Use JDK's Unix domain socket for bootserver --- build.sbt | 8 +- .../java/sbt/internal/BootServerSocket.java | 149 +++++++----------- .../sbt/internal/BootServerSocketProbe.scala | 24 ++- .../sbt/internal/client/NetworkClient.scala | 9 +- .../sbt/internal/BootServerSocketSpec.scala | 5 +- main/src/main/scala/sbt/Defaults.scala | 2 +- main/src/main/scala/sbt/Main.scala | 2 +- .../scala/sbt/protocol/ClientSocket.scala | 3 + 8 files changed, 94 insertions(+), 108 deletions(-) diff --git a/build.sbt b/build.sbt index be5d01642..5de8bfe94 100644 --- a/build.sbt +++ b/build.sbt @@ -634,8 +634,7 @@ lazy val commandProj = (project in file("main-command")) exclude[MissingClassProblem]("sbt.internal.client.ServerConnection"), exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.connection"), exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.init"), - exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.this"), - exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.socketLocation"), + exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.*"), ), Compile / headerCreate / unmanagedSources := { val old = (Compile / headerCreate / unmanagedSources).value @@ -643,6 +642,11 @@ lazy val commandProj = (project in file("main-command")) x.getName.startsWith("NG") || (x.getName == "ReferenceCountedFileDescriptor.java") } }, + // BootServerSocket.java uses JDK 16+ Unix domain socket APIs (StandardProtocolFamily.UNIX, + // UnixDomainSocketAddress, ServerSocketChannel); override the build-wide -source/-target 1.8. + compile / javacOptions ~= { opts => + opts.filterNot(v => v == "-source" || v == "-target" || v == "1.8") ++ Seq("--release", "17") + }, ) .dependsOn(lmCore) .configure( diff --git a/main-command/src/main/java/sbt/internal/BootServerSocket.java b/main-command/src/main/java/sbt/internal/BootServerSocket.java index b7f4353bd..fe17cd754 100644 --- a/main-command/src/main/java/sbt/internal/BootServerSocket.java +++ b/main-command/src/main/java/sbt/internal/BootServerSocket.java @@ -8,14 +8,16 @@ package sbt.internal; +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketException; -import java.net.SocketTimeoutException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.Channels; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -28,9 +30,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.scalasbt.ipcsocket.UnixDomainServerSocket; -import org.scalasbt.ipcsocket.Win32NamedPipeServerSocket; -import org.scalasbt.ipcsocket.Win32SecurityLevel; import sbt.internal.util.Terminal; import xsbti.AppConfiguration; @@ -50,13 +49,12 @@ import xsbti.AppConfiguration; * will not be forthcoming. * *

To address these issues, the BootServerSocket can be used to immediately create a server - * socket before sbt even starts loading the build. It works by creating a local socket either in - * project/target/SOCK_NAME or a windows named pipe with name SOCK_NAME where SOCK_NAME is computed - * as the hash of the project's base directory (for disambiguation in the windows case). If the - * server can't create a server socket because there is already one running, it either prompts the - * user if they want to start a new server even if there is already one running if there is a - * console available or exits with the status code 2 which indicates that there is another sbt - * process starting up. + * socket before sbt even starts loading the build. It works by creating a local Unix domain socket + * at a path under XDG_RUNTIME_DIR (or java.io.tmpdir) on all platforms, including Windows 10+ which + * supports AF_UNIX via JDK 16+. If the server can't create a server socket because there is already + * one running, it either prompts the user if they want to start a new server even if there is + * already one running if there is a console available or exits with the status code 2 which + * indicates that there is another sbt process starting up. * *

Once the server socket is created, it listens for new client connections. When a client * connects, the server will forward its input and output to the client via Terminal.setBootStreams @@ -69,7 +67,7 @@ import xsbti.AppConfiguration; *

BootServerSocket is implemented in java so that it can be classloaded as quickly as possible. */ public class BootServerSocket implements AutoCloseable { - private ServerSocket serverSocket = null; + private ServerSocketChannel serverChannel = null; private final AtomicBoolean closed = new AtomicBoolean(false); private final AtomicBoolean running = new AtomicBoolean(false); private final AtomicInteger threadId = new AtomicInteger(1); @@ -83,16 +81,20 @@ public class BootServerSocket implements AutoCloseable { private final Path socketFile; private final AtomicBoolean needInput = new AtomicBoolean(false); + @SuppressWarnings("deprecation") private class ClientSocket implements AutoCloseable { - final Socket socket; + private final InputStream in; + private final OutputStream out; + private final Closeable closeable; final AtomicBoolean alive = new AtomicBoolean(true); final Future future; private final LinkedBlockingQueue bytes = new LinkedBlockingQueue(); private final AtomicBoolean closed = new AtomicBoolean(false); - @SuppressWarnings("deprecation") - ClientSocket(final Socket socket) { - this.socket = socket; + private ClientSocket(final InputStream in, final OutputStream out, final Closeable closeable) { + this.in = in; + this.out = out; + this.closeable = closeable; clientSockets.add(this); Future f = null; try { @@ -110,15 +112,14 @@ public class BootServerSocket implements AutoCloseable { } return 0; }); - final InputStream inputStream = socket.getInputStream(); while (alive.get()) { try { synchronized (needInput) { while (!needInput.get() && alive.get()) needInput.wait(); } if (alive.get()) { - socket.getOutputStream().write(5); - int b = inputStream.read(); + out.write(5); + int b = in.read(); if (b != -1) { bytes.put(b); clientSocketReads.put(ClientSocket.this); @@ -147,7 +148,7 @@ public class BootServerSocket implements AutoCloseable { private void write(final int i) { try { - if (alive.get()) socket.getOutputStream().write(i); + if (alive.get()) out.write(i); } catch (final IOException e) { alive.set(false); close(); @@ -156,7 +157,7 @@ public class BootServerSocket implements AutoCloseable { private void write(final byte[] b) { try { - if (alive.get()) socket.getOutputStream().write(b); + if (alive.get()) out.write(b); } catch (final IOException e) { alive.set(false); close(); @@ -165,7 +166,7 @@ public class BootServerSocket implements AutoCloseable { private void write(final byte[] b, final int offset, final int len) { try { - if (alive.get()) socket.getOutputStream().write(b, offset, len); + if (alive.get()) out.write(b, offset, len); } catch (final IOException e) { alive.set(false); close(); @@ -174,7 +175,7 @@ public class BootServerSocket implements AutoCloseable { private void flush() { try { - socket.getOutputStream().flush(); + out.flush(); } catch (final IOException e) { alive.set(false); close(); @@ -195,11 +196,7 @@ public class BootServerSocket implements AutoCloseable { alive.set(false); if (future != null) future.cancel(true); try { - socket.getOutputStream().close(); - socket.getInputStream().close(); - // Windows is very slow to close the socket for whatever reason - // We close the server socket anyway, so this should die then. - if (!System.getProperty("os.name", "").toLowerCase().startsWith("win")) socket.close(); + closeable.close(); } catch (final IOException e) { } clientSockets.remove(this); @@ -267,57 +264,40 @@ public class BootServerSocket implements AutoCloseable { return outputStream; } + // Blocking accept; close() interrupts it via AsynchronousCloseException. private final Runnable acceptRunnable = () -> { - try { - serverSocket.setSoTimeout(5000); - while (running.get()) { - try { - ClientSocket clientSocket = new ClientSocket(serverSocket.accept()); - } catch (final SocketTimeoutException e) { - } catch (final IOException e) { - running.set(false); + while (running.get()) { + try { + final SocketChannel sc = serverChannel.accept(); + if (sc != null) { + new ClientSocket(Channels.newInputStream(sc), Channels.newOutputStream(sc), sc); } + } catch (final IOException e) { + running.set(false); } - } catch (final SocketException e) { } }; public BootServerSocket(final AppConfiguration configuration, final long farmHash) throws ServerAlreadyBootingException, IOException { final Path base = configuration.baseDirectory().toPath().toRealPath(); - if (!isWindows) { - final String actualSocketLocation = socketLocation(base, farmHash); - final Path target = Paths.get(actualSocketLocation).getParent(); - if (!Files.isDirectory(target)) Files.createDirectories(target); - socketFile = Paths.get(actualSocketLocation); - } else { - socketFile = null; - } - serverSocket = newSocket(socketLocation(base, farmHash)); - if (serverSocket != null) { - running.set(true); - acceptFuture = service.submit(acceptRunnable); - } else { - closed.set(true); - acceptFuture = null; - } + final String socketPath = socketLocation(base, farmHash); + final Path target = Paths.get(socketPath).getParent(); + if (!Files.isDirectory(target)) Files.createDirectories(target); + socketFile = Paths.get(socketPath); + serverChannel = newChannel(socketPath); + running.set(true); + acceptFuture = service.submit(acceptRunnable); } public static String socketLocation(final Path base, final long farmHash) throws UnsupportedEncodingException, IOException { - final Path target = base.resolve("project").resolve("target"); - if (isWindows) { - return "sbt-load" + farmHash; - } else { - final String alternativeSocketLocation = - System.getenv().getOrDefault("XDG_RUNTIME_DIR", System.getProperty("java.io.tmpdir")); - final Path alternativeSocketLocationRoot = - Paths.get(alternativeSocketLocation).resolve(".sbt"); - final Path locationForSocket = alternativeSocketLocationRoot.resolve("sbt-socket" + farmHash); - final Path pathForSocket = locationForSocket.resolve("sbt-load.sock"); - return pathForSocket.toString(); - } + final String runtimeDir = + System.getenv().getOrDefault("XDG_RUNTIME_DIR", System.getProperty("java.io.tmpdir")); + final Path locationForSocket = + Paths.get(runtimeDir).resolve(".sbt").resolve("sbt-socket" + farmHash); + return locationForSocket.resolve("sbt-load.sock").toString(); } @SuppressWarnings("EmptyCatchBlock") @@ -329,7 +309,7 @@ public class BootServerSocket implements AutoCloseable { if (acceptFuture != null) acceptFuture.cancel(true); service.shutdownNow(); try { - if (serverSocket != null) serverSocket.close(); + serverChannel.close(); } catch (final IOException e) { } try { @@ -339,31 +319,18 @@ public class BootServerSocket implements AutoCloseable { } } - static final boolean isWindows = - System.getProperty("os.name", "").toLowerCase().startsWith("win"); - - static ServerSocket newSocket(final String sock) throws ServerAlreadyBootingException { - ServerSocket socket = null; - String name = socketName(sock); - boolean jni = requiresJNI() || System.getProperty("sbt.ipcsocket.jni", "false").equals("true"); + /** + * Creates a Unix domain ServerSocketChannel at the given path, replacing any stale socket file. + * Throws ServerAlreadyBootingException if the channel cannot be bound. + */ + static ServerSocketChannel newChannel(final String sock) throws ServerAlreadyBootingException { try { - if (!isWindows) Files.deleteIfExists(Paths.get(sock)); - socket = - isWindows - ? new Win32NamedPipeServerSocket(name, jni, Win32SecurityLevel.OWNER_DACL) - : new UnixDomainServerSocket(name, jni); - return socket; + Files.deleteIfExists(Paths.get(sock)); + final ServerSocketChannel channel = ServerSocketChannel.open(StandardProtocolFamily.UNIX); + channel.bind(UnixDomainSocketAddress.of(sock)); + return channel; } catch (final IOException e) { throw new ServerAlreadyBootingException(e); } } - - public static Boolean requiresJNI() { - final boolean isMac = System.getProperty("os.name").toLowerCase().startsWith("mac"); - return isMac && !System.getProperty("os.arch", "").equals("x86_64"); - } - - private static String socketName(String sock) { - return isWindows ? "\\\\.\\pipe\\" + sock : sock; - } } diff --git a/main-command/src/main/scala/sbt/internal/BootServerSocketProbe.scala b/main-command/src/main/scala/sbt/internal/BootServerSocketProbe.scala index d5987c500..56bc767d0 100644 --- a/main-command/src/main/scala/sbt/internal/BootServerSocketProbe.scala +++ b/main-command/src/main/scala/sbt/internal/BootServerSocketProbe.scala @@ -8,9 +8,13 @@ package sbt.internal +import java.net.{ StandardProtocolFamily, UnixDomainSocketAddress } +import java.nio.channels.SocketChannel import java.util.concurrent.{ CountDownLatch, TimeUnit } import java.util.concurrent.atomic.AtomicBoolean +import sbt.internal.util.Util import sbt.protocol.ClientSocket +import scala.util.Using import scala.util.control.NonFatal private[sbt] object BootServerSocketProbe: @@ -20,8 +24,11 @@ private[sbt] object BootServerSocketProbe: * True only if something answers on the boot socket at `location`. A live server answers * immediately, so the connect runs on a daemon thread bounded by [[timeoutMillis]]: the * underlying native connect has no timeout and blocks indefinitely against a bound socket whose - * listen backlog is saturated, which must never hang startup. LinkageError is caught alongside - * NonFatal because the connect may perform the JVM's first JNI/JNA load. + * listen backlog is saturated, which must never hang startup. + * + * Primary: connects via JDK 17 SocketChannel (Unix domain socket, works on all platforms). + * Windows fallback: if the primary fails, also tries [[ClientSocket.localSocket]] (named pipe) + * to detect older sbt servers that pre-date Unix domain socket boot sockets. */ def liveServerDetected(location: String, useJni: Boolean): Boolean = val answered = new AtomicBoolean(false) @@ -29,9 +36,16 @@ private[sbt] object BootServerSocketProbe: val t = new Thread( () => try - ClientSocket.localSocket(location, useJni).close() - answered.set(true) - catch case NonFatal(_) | (_: LinkageError) => () + try + Using.resource(SocketChannel.open(StandardProtocolFamily.UNIX)): ch => + ch.connect(UnixDomainSocketAddress.of(location)) + answered.set(true) + catch case NonFatal(_) | (_: LinkageError) => () + if Util.isWindows && !answered.get() then + try + ClientSocket.localSocket(location, useJni).close() + answered.set(true) + catch case NonFatal(_) | (_: LinkageError) => () finally done.countDown(), "sbt-boot-socket-probe" ) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index e917247b6..3dc6ebeab 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -352,7 +352,7 @@ class NetworkClient( * scenarios. */ var socket: Option[Socket] = - if (!Properties.isLinux) Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption + if (!Properties.isLinux) Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption else None val term = Terminal.console term.exitRawMode() @@ -436,7 +436,7 @@ class NetworkClient( if (!startServer) { val deadline = 5.seconds.fromNow while (socket.isEmpty && !deadline.isOverdue()) { - socket = Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption + socket = Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption if (socket.isEmpty) Thread.sleep(20) } } @@ -457,7 +457,7 @@ class NetworkClient( val buffer = mutable.ArrayBuffer.empty[Byte] while (readThreadAlive.get) { if (socket.isEmpty) { - socket = Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption + socket = Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption } socket.foreach { s => try { @@ -1551,7 +1551,8 @@ object NetworkClient { val out = if (redirectOutput) err else new PrintStream(term.outputStream) val args = parseArgs(arguments.toArray).withBaseDirectory(configuration.baseDirectory) val useJNI = - BootServerSocket.requiresJNI || System.getProperty("sbt.ipcsocket.jni", "false") == "true" + (Util.isMac && sys.props.getOrElse("os.arch", "") != "x86_64") || + System.getProperty("sbt.ipcsocket.jni", "false") == "true" val client = simpleClient(args, term.inputStream, out, err, useJNI = useJNI) clientImpl(client, args.bsp) } diff --git a/main-command/src/test/scala/sbt/internal/BootServerSocketSpec.scala b/main-command/src/test/scala/sbt/internal/BootServerSocketSpec.scala index b4a4f1b09..c8daf5dcd 100644 --- a/main-command/src/test/scala/sbt/internal/BootServerSocketSpec.scala +++ b/main-command/src/test/scala/sbt/internal/BootServerSocketSpec.scala @@ -22,11 +22,8 @@ object BootServerSocketSpec extends BasicTestSuite: override def provider(): xsbti.AppProvider = null } - private def useJni: Boolean = - BootServerSocket.requiresJNI() || sys.props.getOrElse("sbt.ipcsocket.jni", "false") == "true" - private def probe(location: String): Boolean = - BootServerSocketProbe.liveServerDetected(location, useJni) + BootServerSocketProbe.liveServerDetected(location, false) private def freshBase(prefix: String): (java.io.File, Long) = val base = Files.createTempDirectory(prefix).toRealPath().toFile diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 07e22218f..bb7a05044 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -402,7 +402,7 @@ object Defaults extends BuildCommon with DefExtra { }, serverHandlers :== Nil, windowsServerSecurityLevel := Win32SecurityLevel.OWNER_DACL, // allows any owner logon session to access the server - serverUseJni := BootServerSocket.requiresJNI || SysProp.serverUseJni, + serverUseJni := SysProp.serverUseJni, fullServerHandlers := Nil, insideCI :== sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") || SysProp.ci, diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index c3b09487a..20b859265 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -160,7 +160,7 @@ private[sbt] object xMain: def liveServerDetected: Boolean = BootServerSocketProbe.liveServerDetected( BootServerSocket.socketLocation(base, hash), - BootServerSocket.requiresJNI() || SysProp.serverUseJni, + SysProp.serverUseJni, ) try Some(new BootServerSocket(configuration, hash)) -> None catch { diff --git a/protocol/src/main/scala/sbt/protocol/ClientSocket.scala b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala index 79f333cc5..08616bb4c 100644 --- a/protocol/src/main/scala/sbt/protocol/ClientSocket.scala +++ b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala @@ -45,4 +45,7 @@ object ClientSocket { def localSocket(name: String, useJNI: Boolean): Socket = if (isWindows) new Win32NamedPipeSocket(s"\\\\.\\pipe\\$name", useJNI) else new UnixDomainSocket(name, useJNI) + + /** Connects to a Unix domain socket at the given path on all platforms (including Windows 10+). */ + def bootSocket(path: String, useJNI: Boolean): Socket = new UnixDomainSocket(path, useJNI) } From 1f6d19258ff470c07d5479b3f58d729a67ca88db Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 11 Jul 2026 01:38:26 -0400 Subject: [PATCH 2/5] Fallback client --- .../sbt/internal/client/NetworkClient.scala | 15 ++++++++++----- .../main/scala/sbt/protocol/ClientSocket.scala | 17 +++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 3dc6ebeab..746ffc4b9 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -337,6 +337,13 @@ class NetworkClient( conn } + private def bootSocketOpt(bootSocketName: String): Option[Socket] = + Try(ClientSocket.bootSocket(bootSocketName)).toOption match + case Some(x) => Some(x) + case None if Util.isWindows => + Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption + case _ => None + /** * Forks another instance of sbt in the background. * This instance must be shutdown explicitly via `sbt -client shutdown` @@ -351,9 +358,7 @@ class NetworkClient( * For unknown reasons, linux sometimes struggles to connect to the socket in some * scenarios. */ - var socket: Option[Socket] = - if (!Properties.isLinux) Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption - else None + var socket: Option[Socket] = bootSocketOpt(bootSocketName) val term = Terminal.console term.exitRawMode() var serverStderrFile: Option[File] = None @@ -436,7 +441,7 @@ class NetworkClient( if (!startServer) { val deadline = 5.seconds.fromNow while (socket.isEmpty && !deadline.isOverdue()) { - socket = Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption + socket = bootSocketOpt(bootSocketName) if (socket.isEmpty) Thread.sleep(20) } } @@ -457,7 +462,7 @@ class NetworkClient( val buffer = mutable.ArrayBuffer.empty[Byte] while (readThreadAlive.get) { if (socket.isEmpty) { - socket = Try(ClientSocket.bootSocket(bootSocketName, useJNI)).toOption + socket = bootSocketOpt(bootSocketName) } socket.foreach { s => try { diff --git a/protocol/src/main/scala/sbt/protocol/ClientSocket.scala b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala index 08616bb4c..8e65ab0df 100644 --- a/protocol/src/main/scala/sbt/protocol/ClientSocket.scala +++ b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala @@ -9,8 +9,9 @@ package sbt package protocol -import java.io.File -import java.net.{ Socket, URI, InetAddress } +import java.io.{ File, InputStream, OutputStream } +import java.net.{ InetAddress, Socket, StandardProtocolFamily, URI, UnixDomainSocketAddress } +import java.nio.channels.{ Channels, SocketChannel } import sjsonnew.BasicJsonProtocol import sjsonnew.support.scalajson.unsafe.{ Parser, Converter } import sjsonnew.shaded.scalajson.ast.unsafe.JValue @@ -46,6 +47,14 @@ object ClientSocket { if (isWindows) new Win32NamedPipeSocket(s"\\\\.\\pipe\\$name", useJNI) else new UnixDomainSocket(name, useJNI) - /** Connects to a Unix domain socket at the given path on all platforms (including Windows 10+). */ - def bootSocket(path: String, useJNI: Boolean): Socket = new UnixDomainSocket(path, useJNI) + def bootSocket(path: String): Socket = + val ch = SocketChannel.open(StandardProtocolFamily.UNIX) + ch.connect(UnixDomainSocketAddress.of(path)) + new Socket: + private val in = Channels.newInputStream(ch) + private val out = Channels.newOutputStream(ch) + override def getInputStream: InputStream = in + override def getOutputStream: OutputStream = out + override def close(): Unit = ch.close() + override def isClosed: Boolean = !ch.isOpen } From 9369e159c112b63d8cf07e71763c97b55989af75 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 16 Jul 2026 01:47:26 -0400 Subject: [PATCH 3/5] Restore named pipe fallback --- .../src/main/java/sbt/internal/BootServerSocket.java | 4 ++++ .../scala/sbt/internal/client/NetworkClient.scala | 11 ++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/main-command/src/main/java/sbt/internal/BootServerSocket.java b/main-command/src/main/java/sbt/internal/BootServerSocket.java index fe17cd754..b608d294d 100644 --- a/main-command/src/main/java/sbt/internal/BootServerSocket.java +++ b/main-command/src/main/java/sbt/internal/BootServerSocket.java @@ -300,6 +300,10 @@ public class BootServerSocket implements AutoCloseable { return locationForSocket.resolve("sbt-load.sock").toString(); } + public static String namedPipeLocation(final long farmHash) { + return "\\\\.\\pipe\\sbt-load" + farmHash; + } + @SuppressWarnings("EmptyCatchBlock") @Override public void close() { diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 746ffc4b9..aba993a2a 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -337,11 +337,11 @@ class NetworkClient( conn } - private def bootSocketOpt(bootSocketName: String): Option[Socket] = + private def bootSocketOpt(bootSocketName: String, namedPipeName: String): Option[Socket] = Try(ClientSocket.bootSocket(bootSocketName)).toOption match case Some(x) => Some(x) case None if Util.isWindows => - Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption + Try(ClientSocket.localSocket(namedPipeName, useJNI)).toOption case _ => None /** @@ -353,12 +353,13 @@ class NetworkClient( val target = base.resolve("project").resolve("target") val hash = HashUtil.farmHash(target.toString().getBytes("UTF-8")) val bootSocketName = BootServerSocket.socketLocation(base, hash) + val namedPipeName = BootServerSocket.namedPipeLocation(hash) /* * For unknown reasons, linux sometimes struggles to connect to the socket in some * scenarios. */ - var socket: Option[Socket] = bootSocketOpt(bootSocketName) + var socket: Option[Socket] = bootSocketOpt(bootSocketName, namedPipeName) val term = Terminal.console term.exitRawMode() var serverStderrFile: Option[File] = None @@ -441,7 +442,7 @@ class NetworkClient( if (!startServer) { val deadline = 5.seconds.fromNow while (socket.isEmpty && !deadline.isOverdue()) { - socket = bootSocketOpt(bootSocketName) + socket = bootSocketOpt(bootSocketName, namedPipeName) if (socket.isEmpty) Thread.sleep(20) } } @@ -462,7 +463,7 @@ class NetworkClient( val buffer = mutable.ArrayBuffer.empty[Byte] while (readThreadAlive.get) { if (socket.isEmpty) { - socket = bootSocketOpt(bootSocketName) + socket = bootSocketOpt(bootSocketName, namedPipeName) } socket.foreach { s => try { From 26b3c5315f9449b27a65a9fc641bc7a1a0cd8dee Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 16 Jul 2026 01:50:46 -0400 Subject: [PATCH 4/5] Catch UnsupportedOperationException --- main/src/main/scala/sbt/Main.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 20b859265..741fd040a 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -183,8 +183,9 @@ private[sbt] object xMain: ) (None, Some(Exit(2))) } - case _: IOException => (None, None) - case _: UnsatisfiedLinkError => (None, None) + case _: IOException => (None, None) + case _: UnsatisfiedLinkError => (None, None) + case _: UnsupportedOperationException => (None, None) } } end xMain From 7492dc9783836facf3d10f76433fae63d80e95cb Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 17 Jul 2026 00:54:44 -0400 Subject: [PATCH 5/5] Test against sbt 1.x build --- .github/workflows/client-test.yml | 18 +++++++++++++++--- .../java/sbt/internal/BootServerSocket.java | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/client-test.yml b/.github/workflows/client-test.yml index 40c6e9d3f..26a4797bb 100644 --- a/.github/workflows/client-test.yml +++ b/.github/workflows/client-test.yml @@ -86,13 +86,25 @@ jobs: command: | # test building sbtn on Windows (sbtw native-image dropped: scopt lazy vals break under Graal) sbt "-Dsbt.io.virtual=false" nativeImage + - name: sbtn test (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: bash + run: | + REPO_ROOT=$(pwd) + set -o xtrace + # smoke test native Image (sbtn) + $REPO_ROOT/client/target/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat about + $REPO_ROOT/client/target/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat shutdown + pushd launcher-package/citest + # smoke test native Image (sbtn) against sbt 1.x build + $REPO_ROOT/client/target/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat about > out.txt + grep -q "Available Plugins" out.txt + $REPO_ROOT/client/target/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat shutdown + popd - name: Client test (Windows) if: ${{ matrix.os == 'windows-latest' }} shell: bash run: | - # smoke test native Image (sbtn) - ./client/target/bin/sbtn --sbt-script=$(pwd)/launcher-package/src/universal/bin/sbt.bat about - ./client/target/bin/sbtn --sbt-script=$(pwd)/launcher-package/src/universal/bin/sbt.bat shutdown # test launcher script echo build using JDK 17 test using JDK 17 and JDK 25 launcher-package/bin/coursier.bat resolve diff --git a/main-command/src/main/java/sbt/internal/BootServerSocket.java b/main-command/src/main/java/sbt/internal/BootServerSocket.java index b608d294d..615e03792 100644 --- a/main-command/src/main/java/sbt/internal/BootServerSocket.java +++ b/main-command/src/main/java/sbt/internal/BootServerSocket.java @@ -301,7 +301,7 @@ public class BootServerSocket implements AutoCloseable { } public static String namedPipeLocation(final long farmHash) { - return "\\\\.\\pipe\\sbt-load" + farmHash; + return "sbt-load" + farmHash; } @SuppressWarnings("EmptyCatchBlock")