mirror of https://github.com/sbt/sbt.git
[2.x] Use JDK's Unix domain socket for bootserver (#9427)
Use JDK's Unix domain socket for bootserver.
This commit is contained in:
parent
4b3fc911a7
commit
3f073b0059
|
|
@ -86,13 +86,25 @@ jobs:
|
||||||
command: |
|
command: |
|
||||||
# test building sbtn on Windows (sbtw native-image dropped: scopt lazy vals break under Graal)
|
# test building sbtn on Windows (sbtw native-image dropped: scopt lazy vals break under Graal)
|
||||||
sbt "-Dsbt.io.virtual=false" nativeImage
|
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/target/out/jvm/u/sbt-client/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat about
|
||||||
|
$REPO_ROOT/target/out/jvm/u/sbt-client/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/target/out/jvm/u/sbt-client/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat about > out.txt
|
||||||
|
grep -q "Available Plugins" out.txt
|
||||||
|
$REPO_ROOT/target/out/jvm/u/sbt-client/bin/sbtn --sbt-script=$REPO_ROOT/launcher-package/src/universal/bin/sbt.bat shutdown
|
||||||
|
popd
|
||||||
- name: Client test (Windows)
|
- name: Client test (Windows)
|
||||||
if: ${{ matrix.os == 'windows-latest' }}
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
# smoke test native Image (sbtn)
|
|
||||||
./target/out/jvm/u/sbt-client/bin/sbtn --sbt-script=$(pwd)/launcher-package/src/universal/bin/sbt.bat about
|
|
||||||
./target/out/jvm/u/sbt-client/bin/sbtn --sbt-script=$(pwd)/launcher-package/src/universal/bin/sbt.bat shutdown
|
|
||||||
# test launcher script
|
# test launcher script
|
||||||
echo build using JDK 17 test using JDK 17 and JDK 25
|
echo build using JDK 17 test using JDK 17 and JDK 25
|
||||||
launcher-package/bin/coursier.bat resolve
|
launcher-package/bin/coursier.bat resolve
|
||||||
|
|
|
||||||
|
|
@ -635,8 +635,7 @@ lazy val commandProj = (project in file("main-command"))
|
||||||
exclude[MissingClassProblem]("sbt.internal.client.ServerConnection"),
|
exclude[MissingClassProblem]("sbt.internal.client.ServerConnection"),
|
||||||
exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.connection"),
|
exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.connection"),
|
||||||
exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.init"),
|
exclude[IncompatibleResultTypeProblem]("sbt.internal.client.NetworkClient.init"),
|
||||||
exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.this"),
|
exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.*"),
|
||||||
exclude[DirectMissingMethodProblem]("sbt.internal.BootServerSocket.socketLocation"),
|
|
||||||
),
|
),
|
||||||
Compile / headerCreate / unmanagedSources := {
|
Compile / headerCreate / unmanagedSources := {
|
||||||
val old = (Compile / headerCreate / unmanagedSources).value
|
val old = (Compile / headerCreate / unmanagedSources).value
|
||||||
|
|
@ -644,6 +643,11 @@ lazy val commandProj = (project in file("main-command"))
|
||||||
x.getName.startsWith("NG") || (x.getName == "ReferenceCountedFileDescriptor.java")
|
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)
|
.dependsOn(lmCore)
|
||||||
.configure(
|
.configure(
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,16 @@
|
||||||
|
|
||||||
package sbt.internal;
|
package sbt.internal;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.ServerSocket;
|
import java.net.StandardProtocolFamily;
|
||||||
import java.net.Socket;
|
import java.net.UnixDomainSocketAddress;
|
||||||
import java.net.SocketException;
|
import java.nio.channels.Channels;
|
||||||
import java.net.SocketTimeoutException;
|
import java.nio.channels.ServerSocketChannel;
|
||||||
|
import java.nio.channels.SocketChannel;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
@ -28,9 +30,6 @@ import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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 sbt.internal.util.Terminal;
|
||||||
import xsbti.AppConfiguration;
|
import xsbti.AppConfiguration;
|
||||||
|
|
||||||
|
|
@ -50,13 +49,12 @@ import xsbti.AppConfiguration;
|
||||||
* will not be forthcoming.
|
* will not be forthcoming.
|
||||||
*
|
*
|
||||||
* <p>To address these issues, the BootServerSocket can be used to immediately create a server
|
* <p>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
|
* socket before sbt even starts loading the build. It works by creating a local Unix domain socket
|
||||||
* project/target/SOCK_NAME or a windows named pipe with name SOCK_NAME where SOCK_NAME is computed
|
* at a path under XDG_RUNTIME_DIR (or java.io.tmpdir) on all platforms, including Windows 10+ which
|
||||||
* as the hash of the project's base directory (for disambiguation in the windows case). If the
|
* supports AF_UNIX via JDK 16+. If the server can't create a server socket because there is already
|
||||||
* server can't create a server socket because there is already one running, it either prompts the
|
* one running, it either prompts the user if they want to start a new server even if there is
|
||||||
* user if they want to start a new server even if there is already one running if there is a
|
* already one running if there is a console available or exits with the status code 2 which
|
||||||
* console available or exits with the status code 2 which indicates that there is another sbt
|
* indicates that there is another sbt process starting up.
|
||||||
* process starting up.
|
|
||||||
*
|
*
|
||||||
* <p>Once the server socket is created, it listens for new client connections. When a client
|
* <p>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
|
* connects, the server will forward its input and output to the client via Terminal.setBootStreams
|
||||||
|
|
@ -69,7 +67,7 @@ import xsbti.AppConfiguration;
|
||||||
* <p>BootServerSocket is implemented in java so that it can be classloaded as quickly as possible.
|
* <p>BootServerSocket is implemented in java so that it can be classloaded as quickly as possible.
|
||||||
*/
|
*/
|
||||||
public class BootServerSocket implements AutoCloseable {
|
public class BootServerSocket implements AutoCloseable {
|
||||||
private ServerSocket serverSocket = null;
|
private ServerSocketChannel serverChannel = null;
|
||||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
private final AtomicInteger threadId = new AtomicInteger(1);
|
private final AtomicInteger threadId = new AtomicInteger(1);
|
||||||
|
|
@ -83,16 +81,20 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
private final Path socketFile;
|
private final Path socketFile;
|
||||||
private final AtomicBoolean needInput = new AtomicBoolean(false);
|
private final AtomicBoolean needInput = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private class ClientSocket implements AutoCloseable {
|
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 AtomicBoolean alive = new AtomicBoolean(true);
|
||||||
final Future<?> future;
|
final Future<?> future;
|
||||||
private final LinkedBlockingQueue<Integer> bytes = new LinkedBlockingQueue<Integer>();
|
private final LinkedBlockingQueue<Integer> bytes = new LinkedBlockingQueue<Integer>();
|
||||||
private final AtomicBoolean closed = new AtomicBoolean(false);
|
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
private ClientSocket(final InputStream in, final OutputStream out, final Closeable closeable) {
|
||||||
ClientSocket(final Socket socket) {
|
this.in = in;
|
||||||
this.socket = socket;
|
this.out = out;
|
||||||
|
this.closeable = closeable;
|
||||||
clientSockets.add(this);
|
clientSockets.add(this);
|
||||||
Future<?> f = null;
|
Future<?> f = null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -110,15 +112,14 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
final InputStream inputStream = socket.getInputStream();
|
|
||||||
while (alive.get()) {
|
while (alive.get()) {
|
||||||
try {
|
try {
|
||||||
synchronized (needInput) {
|
synchronized (needInput) {
|
||||||
while (!needInput.get() && alive.get()) needInput.wait();
|
while (!needInput.get() && alive.get()) needInput.wait();
|
||||||
}
|
}
|
||||||
if (alive.get()) {
|
if (alive.get()) {
|
||||||
socket.getOutputStream().write(5);
|
out.write(5);
|
||||||
int b = inputStream.read();
|
int b = in.read();
|
||||||
if (b != -1) {
|
if (b != -1) {
|
||||||
bytes.put(b);
|
bytes.put(b);
|
||||||
clientSocketReads.put(ClientSocket.this);
|
clientSocketReads.put(ClientSocket.this);
|
||||||
|
|
@ -147,7 +148,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
|
|
||||||
private void write(final int i) {
|
private void write(final int i) {
|
||||||
try {
|
try {
|
||||||
if (alive.get()) socket.getOutputStream().write(i);
|
if (alive.get()) out.write(i);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
alive.set(false);
|
alive.set(false);
|
||||||
close();
|
close();
|
||||||
|
|
@ -156,7 +157,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
|
|
||||||
private void write(final byte[] b) {
|
private void write(final byte[] b) {
|
||||||
try {
|
try {
|
||||||
if (alive.get()) socket.getOutputStream().write(b);
|
if (alive.get()) out.write(b);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
alive.set(false);
|
alive.set(false);
|
||||||
close();
|
close();
|
||||||
|
|
@ -165,7 +166,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
|
|
||||||
private void write(final byte[] b, final int offset, final int len) {
|
private void write(final byte[] b, final int offset, final int len) {
|
||||||
try {
|
try {
|
||||||
if (alive.get()) socket.getOutputStream().write(b, offset, len);
|
if (alive.get()) out.write(b, offset, len);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
alive.set(false);
|
alive.set(false);
|
||||||
close();
|
close();
|
||||||
|
|
@ -174,7 +175,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
|
|
||||||
private void flush() {
|
private void flush() {
|
||||||
try {
|
try {
|
||||||
socket.getOutputStream().flush();
|
out.flush();
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
alive.set(false);
|
alive.set(false);
|
||||||
close();
|
close();
|
||||||
|
|
@ -195,11 +196,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
alive.set(false);
|
alive.set(false);
|
||||||
if (future != null) future.cancel(true);
|
if (future != null) future.cancel(true);
|
||||||
try {
|
try {
|
||||||
socket.getOutputStream().close();
|
closeable.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();
|
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
}
|
}
|
||||||
clientSockets.remove(this);
|
clientSockets.remove(this);
|
||||||
|
|
@ -267,57 +264,44 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
return outputStream;
|
return outputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Blocking accept; close() interrupts it via AsynchronousCloseException.
|
||||||
private final Runnable acceptRunnable =
|
private final Runnable acceptRunnable =
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
while (running.get()) {
|
||||||
serverSocket.setSoTimeout(5000);
|
try {
|
||||||
while (running.get()) {
|
final SocketChannel sc = serverChannel.accept();
|
||||||
try {
|
if (sc != null) {
|
||||||
ClientSocket clientSocket = new ClientSocket(serverSocket.accept());
|
new ClientSocket(Channels.newInputStream(sc), Channels.newOutputStream(sc), sc);
|
||||||
} catch (final SocketTimeoutException e) {
|
|
||||||
} catch (final IOException e) {
|
|
||||||
running.set(false);
|
|
||||||
}
|
}
|
||||||
|
} catch (final IOException e) {
|
||||||
|
running.set(false);
|
||||||
}
|
}
|
||||||
} catch (final SocketException e) {
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public BootServerSocket(final AppConfiguration configuration, final long farmHash)
|
public BootServerSocket(final AppConfiguration configuration, final long farmHash)
|
||||||
throws ServerAlreadyBootingException, IOException {
|
throws ServerAlreadyBootingException, IOException {
|
||||||
final Path base = configuration.baseDirectory().toPath().toRealPath();
|
final Path base = configuration.baseDirectory().toPath().toRealPath();
|
||||||
if (!isWindows) {
|
final String socketPath = socketLocation(base, farmHash);
|
||||||
final String actualSocketLocation = socketLocation(base, farmHash);
|
final Path target = Paths.get(socketPath).getParent();
|
||||||
final Path target = Paths.get(actualSocketLocation).getParent();
|
if (!Files.isDirectory(target)) Files.createDirectories(target);
|
||||||
if (!Files.isDirectory(target)) Files.createDirectories(target);
|
socketFile = Paths.get(socketPath);
|
||||||
socketFile = Paths.get(actualSocketLocation);
|
serverChannel = newChannel(socketPath);
|
||||||
} else {
|
running.set(true);
|
||||||
socketFile = null;
|
acceptFuture = service.submit(acceptRunnable);
|
||||||
}
|
|
||||||
serverSocket = newSocket(socketLocation(base, farmHash));
|
|
||||||
if (serverSocket != null) {
|
|
||||||
running.set(true);
|
|
||||||
acceptFuture = service.submit(acceptRunnable);
|
|
||||||
} else {
|
|
||||||
closed.set(true);
|
|
||||||
acceptFuture = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String socketLocation(final Path base, final long farmHash)
|
public static String socketLocation(final Path base, final long farmHash)
|
||||||
throws UnsupportedEncodingException, IOException {
|
throws UnsupportedEncodingException, IOException {
|
||||||
final Path target = base.resolve("project").resolve("target");
|
final String runtimeDir =
|
||||||
if (isWindows) {
|
System.getenv().getOrDefault("XDG_RUNTIME_DIR", System.getProperty("java.io.tmpdir"));
|
||||||
return "sbt-load" + farmHash;
|
final Path locationForSocket =
|
||||||
} else {
|
Paths.get(runtimeDir).resolve(".sbt").resolve("sbt-socket" + farmHash);
|
||||||
final String alternativeSocketLocation =
|
return locationForSocket.resolve("sbt-load.sock").toString();
|
||||||
System.getenv().getOrDefault("XDG_RUNTIME_DIR", System.getProperty("java.io.tmpdir"));
|
}
|
||||||
final Path alternativeSocketLocationRoot =
|
|
||||||
Paths.get(alternativeSocketLocation).resolve(".sbt");
|
public static String namedPipeLocation(final long farmHash) {
|
||||||
final Path locationForSocket = alternativeSocketLocationRoot.resolve("sbt-socket" + farmHash);
|
return "sbt-load" + farmHash;
|
||||||
final Path pathForSocket = locationForSocket.resolve("sbt-load.sock");
|
|
||||||
return pathForSocket.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("EmptyCatchBlock")
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
|
|
@ -329,7 +313,7 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
if (acceptFuture != null) acceptFuture.cancel(true);
|
if (acceptFuture != null) acceptFuture.cancel(true);
|
||||||
service.shutdownNow();
|
service.shutdownNow();
|
||||||
try {
|
try {
|
||||||
if (serverSocket != null) serverSocket.close();
|
serverChannel.close();
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -339,31 +323,18 @@ public class BootServerSocket implements AutoCloseable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static final boolean isWindows =
|
/**
|
||||||
System.getProperty("os.name", "").toLowerCase().startsWith("win");
|
* Creates a Unix domain ServerSocketChannel at the given path, replacing any stale socket file.
|
||||||
|
* Throws ServerAlreadyBootingException if the channel cannot be bound.
|
||||||
static ServerSocket newSocket(final String sock) throws ServerAlreadyBootingException {
|
*/
|
||||||
ServerSocket socket = null;
|
static ServerSocketChannel newChannel(final String sock) throws ServerAlreadyBootingException {
|
||||||
String name = socketName(sock);
|
|
||||||
boolean jni = requiresJNI() || System.getProperty("sbt.ipcsocket.jni", "false").equals("true");
|
|
||||||
try {
|
try {
|
||||||
if (!isWindows) Files.deleteIfExists(Paths.get(sock));
|
Files.deleteIfExists(Paths.get(sock));
|
||||||
socket =
|
final ServerSocketChannel channel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
|
||||||
isWindows
|
channel.bind(UnixDomainSocketAddress.of(sock));
|
||||||
? new Win32NamedPipeServerSocket(name, jni, Win32SecurityLevel.OWNER_DACL)
|
return channel;
|
||||||
: new UnixDomainServerSocket(name, jni);
|
|
||||||
return socket;
|
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
throw new ServerAlreadyBootingException(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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,13 @@
|
||||||
|
|
||||||
package sbt.internal
|
package sbt.internal
|
||||||
|
|
||||||
|
import java.net.{ StandardProtocolFamily, UnixDomainSocketAddress }
|
||||||
|
import java.nio.channels.SocketChannel
|
||||||
import java.util.concurrent.{ CountDownLatch, TimeUnit }
|
import java.util.concurrent.{ CountDownLatch, TimeUnit }
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import sbt.internal.util.Util
|
||||||
import sbt.protocol.ClientSocket
|
import sbt.protocol.ClientSocket
|
||||||
|
import scala.util.Using
|
||||||
import scala.util.control.NonFatal
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
private[sbt] object BootServerSocketProbe:
|
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
|
* 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
|
* 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
|
* 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
|
* listen backlog is saturated, which must never hang startup.
|
||||||
* NonFatal because the connect may perform the JVM's first JNI/JNA load.
|
*
|
||||||
|
* 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 =
|
def liveServerDetected(location: String, useJni: Boolean): Boolean =
|
||||||
val answered = new AtomicBoolean(false)
|
val answered = new AtomicBoolean(false)
|
||||||
|
|
@ -29,9 +36,16 @@ private[sbt] object BootServerSocketProbe:
|
||||||
val t = new Thread(
|
val t = new Thread(
|
||||||
() =>
|
() =>
|
||||||
try
|
try
|
||||||
ClientSocket.localSocket(location, useJni).close()
|
try
|
||||||
answered.set(true)
|
Using.resource(SocketChannel.open(StandardProtocolFamily.UNIX)): ch =>
|
||||||
catch case NonFatal(_) | (_: LinkageError) => ()
|
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(),
|
finally done.countDown(),
|
||||||
"sbt-boot-socket-probe"
|
"sbt-boot-socket-probe"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,13 @@ class NetworkClient(
|
||||||
conn
|
conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(namedPipeName, useJNI)).toOption
|
||||||
|
case _ => None
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Forks another instance of sbt in the background.
|
* Forks another instance of sbt in the background.
|
||||||
* This instance must be shutdown explicitly via `sbt -client shutdown`
|
* This instance must be shutdown explicitly via `sbt -client shutdown`
|
||||||
|
|
@ -351,14 +358,13 @@ class NetworkClient(
|
||||||
val target = base.resolve("project").resolve("target")
|
val target = base.resolve("project").resolve("target")
|
||||||
val hash = HashUtil.farmHash(target.toString().getBytes("UTF-8"))
|
val hash = HashUtil.farmHash(target.toString().getBytes("UTF-8"))
|
||||||
val bootSocketName = BootServerSocket.socketLocation(base, hash)
|
val bootSocketName = BootServerSocket.socketLocation(base, hash)
|
||||||
|
val namedPipeName = BootServerSocket.namedPipeLocation(hash)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* For unknown reasons, linux sometimes struggles to connect to the socket in some
|
* For unknown reasons, linux sometimes struggles to connect to the socket in some
|
||||||
* scenarios.
|
* scenarios.
|
||||||
*/
|
*/
|
||||||
var socket: Option[Socket] =
|
var socket: Option[Socket] = bootSocketOpt(bootSocketName, namedPipeName)
|
||||||
if (!Properties.isLinux) Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption
|
|
||||||
else None
|
|
||||||
val term = Terminal.console
|
val term = Terminal.console
|
||||||
term.exitRawMode()
|
term.exitRawMode()
|
||||||
var serverStderrFile: Option[File] = None
|
var serverStderrFile: Option[File] = None
|
||||||
|
|
@ -428,7 +434,7 @@ class NetworkClient(
|
||||||
if (!startServer) {
|
if (!startServer) {
|
||||||
val deadline = 5.seconds.fromNow
|
val deadline = 5.seconds.fromNow
|
||||||
while (socket.isEmpty && !deadline.isOverdue()) {
|
while (socket.isEmpty && !deadline.isOverdue()) {
|
||||||
socket = Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption
|
socket = bootSocketOpt(bootSocketName, namedPipeName)
|
||||||
if (socket.isEmpty) Thread.sleep(20)
|
if (socket.isEmpty) Thread.sleep(20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,7 +455,7 @@ class NetworkClient(
|
||||||
val buffer = mutable.ArrayBuffer.empty[Byte]
|
val buffer = mutable.ArrayBuffer.empty[Byte]
|
||||||
while (readThreadAlive.get) {
|
while (readThreadAlive.get) {
|
||||||
if (socket.isEmpty) {
|
if (socket.isEmpty) {
|
||||||
socket = Try(ClientSocket.localSocket(bootSocketName, useJNI)).toOption
|
socket = bootSocketOpt(bootSocketName, namedPipeName)
|
||||||
}
|
}
|
||||||
socket.foreach { s =>
|
socket.foreach { s =>
|
||||||
try {
|
try {
|
||||||
|
|
@ -1592,7 +1598,8 @@ object NetworkClient {
|
||||||
val out = if (redirectOutput) err else new PrintStream(term.outputStream)
|
val out = if (redirectOutput) err else new PrintStream(term.outputStream)
|
||||||
val args = parseArgs(arguments.toArray).withBaseDirectory(configuration.baseDirectory)
|
val args = parseArgs(arguments.toArray).withBaseDirectory(configuration.baseDirectory)
|
||||||
val useJNI =
|
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)
|
val client = simpleClient(args, term.inputStream, out, err, useJNI = useJNI)
|
||||||
clientImpl(client, args.bsp)
|
clientImpl(client, args.bsp)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,8 @@ object BootServerSocketSpec extends BasicTestSuite:
|
||||||
override def provider(): xsbti.AppProvider = null
|
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 =
|
private def probe(location: String): Boolean =
|
||||||
BootServerSocketProbe.liveServerDetected(location, useJni)
|
BootServerSocketProbe.liveServerDetected(location, false)
|
||||||
|
|
||||||
private def freshBase(prefix: String): (java.io.File, Long) =
|
private def freshBase(prefix: String): (java.io.File, Long) =
|
||||||
val base = Files.createTempDirectory(prefix).toRealPath().toFile
|
val base = Files.createTempDirectory(prefix).toRealPath().toFile
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,7 @@ object Defaults extends BuildCommon with DefExtra {
|
||||||
},
|
},
|
||||||
serverHandlers :== Nil,
|
serverHandlers :== Nil,
|
||||||
windowsServerSecurityLevel := Win32SecurityLevel.OWNER_DACL, // allows any owner logon session to access the server
|
windowsServerSecurityLevel := Win32SecurityLevel.OWNER_DACL, // allows any owner logon session to access the server
|
||||||
serverUseJni := BootServerSocket.requiresJNI || SysProp.serverUseJni,
|
serverUseJni := SysProp.serverUseJni,
|
||||||
fullServerHandlers := Nil,
|
fullServerHandlers := Nil,
|
||||||
insideCI :== sys.env.contains("BUILD_NUMBER") ||
|
insideCI :== sys.env.contains("BUILD_NUMBER") ||
|
||||||
sys.env.contains("CI") || SysProp.ci,
|
sys.env.contains("CI") || SysProp.ci,
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ private[sbt] object xMain:
|
||||||
def liveServerDetected: Boolean =
|
def liveServerDetected: Boolean =
|
||||||
BootServerSocketProbe.liveServerDetected(
|
BootServerSocketProbe.liveServerDetected(
|
||||||
BootServerSocket.socketLocation(base, hash),
|
BootServerSocket.socketLocation(base, hash),
|
||||||
BootServerSocket.requiresJNI() || SysProp.serverUseJni,
|
SysProp.serverUseJni,
|
||||||
)
|
)
|
||||||
try Some(new BootServerSocket(configuration, hash)) -> None
|
try Some(new BootServerSocket(configuration, hash)) -> None
|
||||||
catch {
|
catch {
|
||||||
|
|
@ -183,8 +183,9 @@ private[sbt] object xMain:
|
||||||
)
|
)
|
||||||
(None, Some(Exit(2)))
|
(None, Some(Exit(2)))
|
||||||
}
|
}
|
||||||
case _: IOException => (None, None)
|
case _: IOException => (None, None)
|
||||||
case _: UnsatisfiedLinkError => (None, None)
|
case _: UnsatisfiedLinkError => (None, None)
|
||||||
|
case _: UnsupportedOperationException => (None, None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end xMain
|
end xMain
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@
|
||||||
package sbt
|
package sbt
|
||||||
package protocol
|
package protocol
|
||||||
|
|
||||||
import java.io.File
|
import java.io.{ File, InputStream, OutputStream }
|
||||||
import java.net.{ Socket, URI, InetAddress }
|
import java.net.{ InetAddress, Socket, StandardProtocolFamily, URI, UnixDomainSocketAddress }
|
||||||
|
import java.nio.channels.{ Channels, SocketChannel }
|
||||||
import sjsonnew.BasicJsonProtocol
|
import sjsonnew.BasicJsonProtocol
|
||||||
import sjsonnew.support.scalajson.unsafe.{ Parser, Converter }
|
import sjsonnew.support.scalajson.unsafe.{ Parser, Converter }
|
||||||
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
|
import sjsonnew.shaded.scalajson.ast.unsafe.JValue
|
||||||
|
|
@ -45,4 +46,15 @@ object ClientSocket {
|
||||||
def localSocket(name: String, useJNI: Boolean): Socket =
|
def localSocket(name: String, useJNI: Boolean): Socket =
|
||||||
if (isWindows) new Win32NamedPipeSocket(s"\\\\.\\pipe\\$name", useJNI)
|
if (isWindows) new Win32NamedPipeSocket(s"\\\\.\\pipe\\$name", useJNI)
|
||||||
else new UnixDomainSocket(name, useJNI)
|
else new UnixDomainSocket(name, 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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue