diff --git a/.appveyor.yml b/.appveyor.yml index a0d3292f1..ccc70ab7c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,9 +4,8 @@ init: - git config --global core.autocrlf input install: - - cinst jdk8 -params 'installdir=C:\\jdk8' - - SET JAVA_HOME=C:\jdk8 - - SET PATH=C:\jdk8\bin;%PATH% + - SET JAVA_HOME=C:\Program Files\Java\jdk1.8.0 + - SET PATH=%JAVA_HOME%\bin;%PATH% - ps: | Add-Type -AssemblyName System.IO.Compression.FileSystem @@ -20,4 +19,8 @@ install: - SET PATH=C:\sbt\sbt\bin;%PATH% - SET SBT_OPTS=-XX:MaxPermSize=2g -Xmx4g -Dfile.encoding=UTF8 test_script: - - sbt "scripted actions/* server/*" + - sbt "scripted actions/*" "testOnly sbt.ServerSpec" + +cache: + - '%USERPROFILE%\.ivy2\cache' + - '%USERPROFILE%\.sbt' diff --git a/.travis.yml b/.travis.yml index a8a539237..c6e2d000e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ env: - SBT_CMD="scripted dependency-management/*4of4" - SBT_CMD="scripted java/* package/* reporter/* run/* project-load/*" - SBT_CMD="scripted project/*1of2" - - SBT_CMD="scripted project/*2of2 server/*" + - SBT_CMD="scripted project/*2of2" - SBT_CMD="scripted source-dependencies/*1of3" - SBT_CMD="scripted source-dependencies/*2of3" - SBT_CMD="scripted source-dependencies/*3of3" diff --git a/build.sbt b/build.sbt index d832e23f4..3cf04650b 100644 --- a/build.sbt +++ b/build.sbt @@ -318,12 +318,13 @@ lazy val actionsProj = (project in file("main-actions")) lazy val protocolProj = (project in file("protocol")) .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(collectionProj) .settings( testedBaseSettings, scalacOptions -= "-Ywarn-unused", scalacOptions += "-Xlint:-unused", name := "Protocol", - libraryDependencies ++= Seq(sjsonNewScalaJson.value), + libraryDependencies ++= Seq(sjsonNewScalaJson.value, ipcSocket), managedSourceDirectories in Compile += baseDirectory.value / "src" / "main" / "contraband-scala", sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", @@ -355,6 +356,9 @@ lazy val commandProj = (project in file("main-command")) exclude[ReversedMissingMethodProblem]("sbt.internal.CommandChannel.*"), // Added an overload to reboot. The overload is private[sbt]. exclude[ReversedMissingMethodProblem]("sbt.StateOps.reboot"), + // Replace nailgun socket stuff + exclude[MissingClassProblem]("sbt.internal.NG*"), + exclude[MissingClassProblem]("sbt.internal.ReferenceCountedFileDescriptor"), ), unmanagedSources in (Compile, headerCreate) := { val old = (unmanagedSources in (Compile, headerCreate)).value @@ -463,7 +467,7 @@ lazy val mainProj = (project in file("main")) lazy val sbtProj = (project in file("sbt")) .dependsOn(mainProj, scriptedSbtProj % "test->test") .settings( - baseSettings, + testedBaseSettings, name := "sbt", normalizedName := "sbt", crossScalaVersions := Seq(baseScalaVersion), @@ -477,6 +481,7 @@ lazy val sbtProj = (project in file("sbt")) buildInfoKeys in Test := Seq[BuildInfoKey](fullClasspath in Compile), connectInput in run in Test := true, outputStrategy in run in Test := Some(StdoutOutput), + fork in Test := true, ) .configure(addSbtCompilerBridge) diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java b/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java deleted file mode 100644 index 89d3bcf43..000000000 --- a/main-command/src/main/java/sbt/internal/NGUnixDomainServerSocket.java +++ /dev/null @@ -1,178 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainServerSocket.java - -/* - - Copyright 2004-2015, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketAddress; -import java.util.concurrent.atomic.AtomicInteger; - -import com.sun.jna.LastErrorException; -import com.sun.jna.ptr.IntByReference; - -/** - * Implements a {@link ServerSocket} which binds to a local Unix domain socket - * and returns instances of {@link NGUnixDomainSocket} from - * {@link #accept()}. - */ -public class NGUnixDomainServerSocket extends ServerSocket { - private static final int DEFAULT_BACKLOG = 50; - - // We use an AtomicInteger to prevent a race in this situation which - // could happen if fd were just an int: - // - // Thread 1 -> NGUnixDomainServerSocket.accept() - // -> lock this - // -> check isBound and isClosed - // -> unlock this - // -> descheduled while still in method - // Thread 2 -> NGUnixDomainServerSocket.close() - // -> lock this - // -> check isClosed - // -> NGUnixDomainSocketLibrary.close(fd) - // -> now fd is invalid - // -> unlock this - // Thread 1 -> re-scheduled while still in method - // -> NGUnixDomainSocketLibrary.accept(fd, which is invalid and maybe re-used) - // - // By using an AtomicInteger, we'll set this to -1 after it's closed, which - // will cause the accept() call above to cleanly fail instead of possibly - // being called on an unrelated fd (which may or may not fail). - private final AtomicInteger fd; - - private final int backlog; - private boolean isBound; - private boolean isClosed; - - public static class NGUnixDomainServerSocketAddress extends SocketAddress { - private final String path; - - public NGUnixDomainServerSocketAddress(String path) { - this.path = path; - } - - public String getPath() { - return path; - } - } - - /** - * Constructs an unbound Unix domain server socket. - */ - public NGUnixDomainServerSocket() throws IOException { - this(DEFAULT_BACKLOG, null); - } - - /** - * Constructs an unbound Unix domain server socket with the specified listen backlog. - */ - public NGUnixDomainServerSocket(int backlog) throws IOException { - this(backlog, null); - } - - /** - * Constructs and binds a Unix domain server socket to the specified path. - */ - public NGUnixDomainServerSocket(String path) throws IOException { - this(DEFAULT_BACKLOG, path); - } - - /** - * Constructs and binds a Unix domain server socket to the specified path - * with the specified listen backlog. - */ - public NGUnixDomainServerSocket(int backlog, String path) throws IOException { - try { - fd = new AtomicInteger( - NGUnixDomainSocketLibrary.socket( - NGUnixDomainSocketLibrary.PF_LOCAL, - NGUnixDomainSocketLibrary.SOCK_STREAM, - 0)); - this.backlog = backlog; - if (path != null) { - bind(new NGUnixDomainServerSocketAddress(path)); - } - } catch (LastErrorException e) { - throw new IOException(e); - } - } - - public synchronized void bind(SocketAddress endpoint) throws IOException { - if (!(endpoint instanceof NGUnixDomainServerSocketAddress)) { - throw new IllegalArgumentException( - "endpoint must be an instance of NGUnixDomainServerSocketAddress"); - } - if (isBound) { - throw new IllegalStateException("Socket is already bound"); - } - if (isClosed) { - throw new IllegalStateException("Socket is already closed"); - } - NGUnixDomainServerSocketAddress unEndpoint = (NGUnixDomainServerSocketAddress) endpoint; - NGUnixDomainSocketLibrary.SockaddrUn address = - new NGUnixDomainSocketLibrary.SockaddrUn(unEndpoint.getPath()); - try { - int socketFd = fd.get(); - NGUnixDomainSocketLibrary.bind(socketFd, address, address.size()); - NGUnixDomainSocketLibrary.listen(socketFd, backlog); - isBound = true; - } catch (LastErrorException e) { - throw new IOException(e); - } - } - - public Socket accept() throws IOException { - // We explicitly do not make this method synchronized, since the - // call to NGUnixDomainSocketLibrary.accept() will block - // indefinitely, causing another thread's call to close() to deadlock. - synchronized (this) { - if (!isBound) { - throw new IllegalStateException("Socket is not bound"); - } - if (isClosed) { - throw new IllegalStateException("Socket is already closed"); - } - } - try { - NGUnixDomainSocketLibrary.SockaddrUn sockaddrUn = - new NGUnixDomainSocketLibrary.SockaddrUn(); - IntByReference addressLen = new IntByReference(); - addressLen.setValue(sockaddrUn.size()); - int clientFd = NGUnixDomainSocketLibrary.accept(fd.get(), sockaddrUn, addressLen); - return new NGUnixDomainSocket(clientFd); - } catch (LastErrorException e) { - throw new IOException(e); - } - } - - public synchronized void close() throws IOException { - if (isClosed) { - throw new IllegalStateException("Socket is already closed"); - } - try { - // Ensure any pending call to accept() fails. - NGUnixDomainSocketLibrary.close(fd.getAndSet(-1)); - isClosed = true; - } catch (LastErrorException e) { - throw new IOException(e); - } - } -} diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java b/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java deleted file mode 100644 index b70bef611..000000000 --- a/main-command/src/main/java/sbt/internal/NGUnixDomainSocket.java +++ /dev/null @@ -1,192 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainSocket.java - -/* - - Copyright 2004-2015, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import com.sun.jna.LastErrorException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import java.nio.ByteBuffer; - -import java.net.Socket; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Implements a {@link Socket} backed by a native Unix domain socket. - * - * Instances of this class always return {@code null} for - * {@link Socket#getInetAddress()}, {@link Socket#getLocalAddress()}, - * {@link Socket#getLocalSocketAddress()}, {@link Socket#getRemoteSocketAddress()}. - */ -public class NGUnixDomainSocket extends Socket { - private final ReferenceCountedFileDescriptor fd; - private final InputStream is; - private final OutputStream os; - - public NGUnixDomainSocket(String path) throws IOException { - try { - AtomicInteger fd = new AtomicInteger( - NGUnixDomainSocketLibrary.socket( - NGUnixDomainSocketLibrary.PF_LOCAL, - NGUnixDomainSocketLibrary.SOCK_STREAM, - 0)); - NGUnixDomainSocketLibrary.SockaddrUn address = - new NGUnixDomainSocketLibrary.SockaddrUn(path); - int socketFd = fd.get(); - NGUnixDomainSocketLibrary.connect(socketFd, address, address.size()); - this.fd = new ReferenceCountedFileDescriptor(socketFd); - this.is = new NGUnixDomainSocketInputStream(); - this.os = new NGUnixDomainSocketOutputStream(); - } catch (LastErrorException e) { - throw new IOException(e); - } - } - - /** - * Creates a Unix domain socket backed by a native file descriptor. - */ - public NGUnixDomainSocket(int fd) { - this.fd = new ReferenceCountedFileDescriptor(fd); - this.is = new NGUnixDomainSocketInputStream(); - this.os = new NGUnixDomainSocketOutputStream(); - } - - public InputStream getInputStream() { - return is; - } - - public OutputStream getOutputStream() { - return os; - } - - public void shutdownInput() throws IOException { - doShutdown(NGUnixDomainSocketLibrary.SHUT_RD); - } - - public void shutdownOutput() throws IOException { - doShutdown(NGUnixDomainSocketLibrary.SHUT_WR); - } - - private void doShutdown(int how) throws IOException { - try { - int socketFd = fd.acquire(); - if (socketFd != -1) { - NGUnixDomainSocketLibrary.shutdown(socketFd, how); - } - } catch (LastErrorException e) { - throw new IOException(e); - } finally { - fd.release(); - } - } - - public void close() throws IOException { - super.close(); - try { - // This might not close the FD right away. In case we are about - // to read or write on another thread, it will delay the close - // until the read or write completes, to prevent the FD from - // being re-used for a different purpose and the other thread - // reading from a different FD. - fd.close(); - } catch (LastErrorException e) { - throw new IOException(e); - } - } - - private class NGUnixDomainSocketInputStream extends InputStream { - public int read() throws IOException { - ByteBuffer buf = ByteBuffer.allocate(1); - int result; - if (doRead(buf) == 0) { - result = -1; - } else { - // Make sure to & with 0xFF to avoid sign extension - result = 0xFF & buf.get(); - } - return result; - } - - public int read(byte[] b, int off, int len) throws IOException { - if (len == 0) { - return 0; - } - ByteBuffer buf = ByteBuffer.wrap(b, off, len); - int result = doRead(buf); - if (result == 0) { - result = -1; - } - return result; - } - - private int doRead(ByteBuffer buf) throws IOException { - try { - int fdToRead = fd.acquire(); - if (fdToRead == -1) { - return -1; - } - return NGUnixDomainSocketLibrary.read(fdToRead, buf, buf.remaining()); - } catch (LastErrorException e) { - throw new IOException(e); - } finally { - fd.release(); - } - } - } - - private class NGUnixDomainSocketOutputStream extends OutputStream { - - public void write(int b) throws IOException { - ByteBuffer buf = ByteBuffer.allocate(1); - buf.put(0, (byte) (0xFF & b)); - doWrite(buf); - } - - public void write(byte[] b, int off, int len) throws IOException { - if (len == 0) { - return; - } - ByteBuffer buf = ByteBuffer.wrap(b, off, len); - doWrite(buf); - } - - private void doWrite(ByteBuffer buf) throws IOException { - try { - int fdToWrite = fd.acquire(); - if (fdToWrite == -1) { - return; - } - int ret = NGUnixDomainSocketLibrary.write(fdToWrite, buf, buf.remaining()); - if (ret != buf.remaining()) { - // This shouldn't happen with standard blocking Unix domain sockets. - throw new IOException("Could not write " + buf.remaining() + " bytes as requested " + - "(wrote " + ret + " bytes instead)"); - } - } catch (LastErrorException e) { - throw new IOException(e); - } finally { - fd.release(); - } - } - } -} diff --git a/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java b/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java deleted file mode 100644 index 4d781b6b6..000000000 --- a/main-command/src/main/java/sbt/internal/NGUnixDomainSocketLibrary.java +++ /dev/null @@ -1,142 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGUnixDomainSocketLibrary.java - -/* - - Copyright 2004-2015, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import com.sun.jna.LastErrorException; -import com.sun.jna.Native; -import com.sun.jna.Platform; -import com.sun.jna.Structure; -import com.sun.jna.Union; -import com.sun.jna.ptr.IntByReference; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.List; - -/** - * Utility class to bridge native Unix domain socket calls to Java using JNA. - */ -public class NGUnixDomainSocketLibrary { - public static final int PF_LOCAL = 1; - public static final int AF_LOCAL = 1; - public static final int SOCK_STREAM = 1; - - public static final int SHUT_RD = 0; - public static final int SHUT_WR = 1; - - // Utility class, do not instantiate. - private NGUnixDomainSocketLibrary() { } - - // BSD platforms write a length byte at the start of struct sockaddr_un. - private static final boolean HAS_SUN_LEN = - Platform.isMac() || Platform.isFreeBSD() || Platform.isNetBSD() || - Platform.isOpenBSD() || Platform.iskFreeBSD(); - - /** - * Bridges {@code struct sockaddr_un} to and from native code. - */ - public static class SockaddrUn extends Structure implements Structure.ByReference { - /** - * On BSD platforms, the {@code sun_len} and {@code sun_family} values in - * {@code struct sockaddr_un}. - */ - public static class SunLenAndFamily extends Structure { - public byte sunLen; - public byte sunFamily; - - protected List getFieldOrder() { - return Arrays.asList(new String[] { "sunLen", "sunFamily" }); - } - } - - /** - * On BSD platforms, {@code sunLenAndFamily} will be present. - * On other platforms, only {@code sunFamily} will be present. - */ - public static class SunFamily extends Union { - public SunLenAndFamily sunLenAndFamily; - public short sunFamily; - } - - public SunFamily sunFamily = new SunFamily(); - public byte[] sunPath = new byte[104]; - - /** - * Constructs an empty {@code struct sockaddr_un}. - */ - public SockaddrUn() { - if (HAS_SUN_LEN) { - sunFamily.sunLenAndFamily = new SunLenAndFamily(); - sunFamily.setType(SunLenAndFamily.class); - } else { - sunFamily.setType(Short.TYPE); - } - allocateMemory(); - } - - /** - * Constructs a {@code struct sockaddr_un} with a path whose bytes are encoded - * using the default encoding of the platform. - */ - public SockaddrUn(String path) throws IOException { - byte[] pathBytes = path.getBytes(); - if (pathBytes.length > sunPath.length - 1) { - throw new IOException("Cannot fit name [" + path + "] in maximum unix domain socket length"); - } - System.arraycopy(pathBytes, 0, sunPath, 0, pathBytes.length); - sunPath[pathBytes.length] = (byte) 0; - if (HAS_SUN_LEN) { - int len = fieldOffset("sunPath") + pathBytes.length; - sunFamily.sunLenAndFamily = new SunLenAndFamily(); - sunFamily.sunLenAndFamily.sunLen = (byte) len; - sunFamily.sunLenAndFamily.sunFamily = AF_LOCAL; - sunFamily.setType(SunLenAndFamily.class); - } else { - sunFamily.sunFamily = AF_LOCAL; - sunFamily.setType(Short.TYPE); - } - allocateMemory(); - } - - protected List getFieldOrder() { - return Arrays.asList(new String[] { "sunFamily", "sunPath" }); - } - } - - static { - Native.register(Platform.C_LIBRARY_NAME); - } - - public static native int socket(int domain, int type, int protocol) throws LastErrorException; - public static native int bind(int fd, SockaddrUn address, int addressLen) - throws LastErrorException; - public static native int listen(int fd, int backlog) throws LastErrorException; - public static native int accept(int fd, SockaddrUn address, IntByReference addressLen) - throws LastErrorException; - public static native int connect(int fd, SockaddrUn address, int addressLen) - throws LastErrorException; - public static native int read(int fd, ByteBuffer buffer, int count) - throws LastErrorException; - public static native int write(int fd, ByteBuffer buffer, int count) - throws LastErrorException; - public static native int close(int fd) throws LastErrorException; - public static native int shutdown(int fd, int how) throws LastErrorException; -} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java deleted file mode 100644 index dd4d8f15a..000000000 --- a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeLibrary.java +++ /dev/null @@ -1,90 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeLibrary.java - -/* - - Copyright 2004-2017, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import java.nio.ByteBuffer; - -import com.sun.jna.*; -import com.sun.jna.platform.win32.WinNT; -import com.sun.jna.platform.win32.WinNT.*; -import com.sun.jna.platform.win32.WinBase.*; -import com.sun.jna.ptr.IntByReference; - -import com.sun.jna.win32.W32APIOptions; - -public interface NGWin32NamedPipeLibrary extends Library, WinNT { - int PIPE_ACCESS_DUPLEX = 3; - int PIPE_UNLIMITED_INSTANCES = 255; - int FILE_FLAG_FIRST_PIPE_INSTANCE = 524288; - - NGWin32NamedPipeLibrary INSTANCE = - (NGWin32NamedPipeLibrary) Native.loadLibrary( - "kernel32", - NGWin32NamedPipeLibrary.class, - W32APIOptions.UNICODE_OPTIONS); - - HANDLE CreateNamedPipe( - String lpName, - int dwOpenMode, - int dwPipeMode, - int nMaxInstances, - int nOutBufferSize, - int nInBufferSize, - int nDefaultTimeOut, - SECURITY_ATTRIBUTES lpSecurityAttributes); - boolean ConnectNamedPipe( - HANDLE hNamedPipe, - Pointer lpOverlapped); - boolean DisconnectNamedPipe( - HANDLE hObject); - boolean ReadFile( - HANDLE hFile, - Memory lpBuffer, - int nNumberOfBytesToRead, - IntByReference lpNumberOfBytesRead, - Pointer lpOverlapped); - boolean WriteFile( - HANDLE hFile, - ByteBuffer lpBuffer, - int nNumberOfBytesToWrite, - IntByReference lpNumberOfBytesWritten, - Pointer lpOverlapped); - boolean CloseHandle( - HANDLE hObject); - boolean GetOverlappedResult( - HANDLE hFile, - Pointer lpOverlapped, - IntByReference lpNumberOfBytesTransferred, - boolean wait); - boolean CancelIoEx( - HANDLE hObject, - Pointer lpOverlapped); - HANDLE CreateEvent( - SECURITY_ATTRIBUTES lpEventAttributes, - boolean bManualReset, - boolean bInitialState, - String lpName); - int WaitForSingleObject( - HANDLE hHandle, - int dwMilliseconds - ); - - int GetLastError(); -} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java deleted file mode 100644 index 137d9b5dc..000000000 --- a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeServerSocket.java +++ /dev/null @@ -1,173 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeServerSocket.java - -/* - - Copyright 2004-2017, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import com.sun.jna.platform.win32.WinBase; -import com.sun.jna.platform.win32.WinError; -import com.sun.jna.platform.win32.WinNT; -import com.sun.jna.platform.win32.WinNT.HANDLE; -import com.sun.jna.ptr.IntByReference; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; - -public class NGWin32NamedPipeServerSocket extends ServerSocket { - private static final NGWin32NamedPipeLibrary API = NGWin32NamedPipeLibrary.INSTANCE; - private static final String WIN32_PIPE_PREFIX = "\\\\.\\pipe\\"; - private static final int BUFFER_SIZE = 65535; - private final LinkedBlockingQueue openHandles; - private final LinkedBlockingQueue connectedHandles; - private final NGWin32NamedPipeSocket.CloseCallback closeCallback; - private final String path; - private final int maxInstances; - private final HANDLE lockHandle; - - public NGWin32NamedPipeServerSocket(String path) throws IOException { - this(NGWin32NamedPipeLibrary.PIPE_UNLIMITED_INSTANCES, path); - } - - public NGWin32NamedPipeServerSocket(int maxInstances, String path) throws IOException { - this.openHandles = new LinkedBlockingQueue<>(); - this.connectedHandles = new LinkedBlockingQueue<>(); - this.closeCallback = handle -> { - if (connectedHandles.remove(handle)) { - closeConnectedPipe(handle, false); - } - if (openHandles.remove(handle)) { - closeOpenPipe(handle); - } - }; - this.maxInstances = maxInstances; - if (!path.startsWith(WIN32_PIPE_PREFIX)) { - this.path = WIN32_PIPE_PREFIX + path; - } else { - this.path = path; - } - String lockPath = this.path + "_lock"; - lockHandle = API.CreateNamedPipe( - lockPath, - NGWin32NamedPipeLibrary.FILE_FLAG_FIRST_PIPE_INSTANCE | NGWin32NamedPipeLibrary.PIPE_ACCESS_DUPLEX, - 0, - 1, - BUFFER_SIZE, - BUFFER_SIZE, - 0, - null); - if (lockHandle == NGWin32NamedPipeLibrary.INVALID_HANDLE_VALUE) { - throw new IOException(String.format("Could not create lock for %s, error %d", lockPath, API.GetLastError())); - } else { - if (!API.DisconnectNamedPipe(lockHandle)) { - throw new IOException(String.format("Could not disconnect lock %d", API.GetLastError())); - } - } - - } - - public void bind(SocketAddress endpoint) throws IOException { - throw new IOException("Win32 named pipes do not support bind(), pass path to constructor"); - } - - public Socket accept() throws IOException { - HANDLE handle = API.CreateNamedPipe( - path, - NGWin32NamedPipeLibrary.PIPE_ACCESS_DUPLEX | WinNT.FILE_FLAG_OVERLAPPED, - 0, - maxInstances, - BUFFER_SIZE, - BUFFER_SIZE, - 0, - null); - if (handle == NGWin32NamedPipeLibrary.INVALID_HANDLE_VALUE) { - throw new IOException(String.format("Could not create named pipe, error %d", API.GetLastError())); - } - openHandles.add(handle); - - HANDLE connWaitable = API.CreateEvent(null, true, false, null); - WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); - olap.hEvent = connWaitable; - olap.write(); - - boolean immediate = API.ConnectNamedPipe(handle, olap.getPointer()); - if (immediate) { - openHandles.remove(handle); - connectedHandles.add(handle); - return new NGWin32NamedPipeSocket(handle, closeCallback); - } - - int connectError = API.GetLastError(); - if (connectError == WinError.ERROR_PIPE_CONNECTED) { - openHandles.remove(handle); - connectedHandles.add(handle); - return new NGWin32NamedPipeSocket(handle, closeCallback); - } else if (connectError == WinError.ERROR_NO_DATA) { - // Client has connected and disconnected between CreateNamedPipe() and ConnectNamedPipe() - // connection is broken, but it is returned it avoid loop here. - // Actual error will happen for NGSession when it will try to read/write from/to pipe - return new NGWin32NamedPipeSocket(handle, closeCallback); - } else if (connectError == WinError.ERROR_IO_PENDING) { - if (!API.GetOverlappedResult(handle, olap.getPointer(), new IntByReference(), true)) { - openHandles.remove(handle); - closeOpenPipe(handle); - throw new IOException("GetOverlappedResult() failed for connect operation: " + API.GetLastError()); - } - openHandles.remove(handle); - connectedHandles.add(handle); - return new NGWin32NamedPipeSocket(handle, closeCallback); - } else { - throw new IOException("ConnectNamedPipe() failed with: " + connectError); - } - } - - public void close() throws IOException { - try { - List handlesToClose = new ArrayList<>(); - openHandles.drainTo(handlesToClose); - for (HANDLE handle : handlesToClose) { - closeOpenPipe(handle); - } - - List handlesToDisconnect = new ArrayList<>(); - connectedHandles.drainTo(handlesToDisconnect); - for (HANDLE handle : handlesToDisconnect) { - closeConnectedPipe(handle, true); - } - } finally { - API.CloseHandle(lockHandle); - } - } - - private void closeOpenPipe(HANDLE handle) throws IOException { - API.CancelIoEx(handle, null); - API.CloseHandle(handle); - } - - private void closeConnectedPipe(HANDLE handle, boolean shutdown) throws IOException { - if (!shutdown) { - API.WaitForSingleObject(handle, 10000); - } - API.DisconnectNamedPipe(handle); - API.CloseHandle(handle); - } -} diff --git a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java b/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java deleted file mode 100644 index b22bb6bbf..000000000 --- a/main-command/src/main/java/sbt/internal/NGWin32NamedPipeSocket.java +++ /dev/null @@ -1,172 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/NGWin32NamedPipeSocket.java -// Made change in `read` to read just the amount of bytes available. - -/* - - Copyright 2004-2017, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import com.sun.jna.Memory; -import com.sun.jna.platform.win32.WinBase; -import com.sun.jna.platform.win32.WinError; -import com.sun.jna.platform.win32.WinNT.HANDLE; -import com.sun.jna.ptr.IntByReference; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.Socket; -import java.nio.ByteBuffer; - -public class NGWin32NamedPipeSocket extends Socket { - private static final NGWin32NamedPipeLibrary API = NGWin32NamedPipeLibrary.INSTANCE; - private final HANDLE handle; - private final CloseCallback closeCallback; - private final InputStream is; - private final OutputStream os; - private final HANDLE readerWaitable; - private final HANDLE writerWaitable; - - interface CloseCallback { - void onNamedPipeSocketClose(HANDLE handle) throws IOException; - } - - public NGWin32NamedPipeSocket( - HANDLE handle, - NGWin32NamedPipeSocket.CloseCallback closeCallback) throws IOException { - this.handle = handle; - this.closeCallback = closeCallback; - this.readerWaitable = API.CreateEvent(null, true, false, null); - if (readerWaitable == null) { - throw new IOException("CreateEvent() failed "); - } - writerWaitable = API.CreateEvent(null, true, false, null); - if (writerWaitable == null) { - throw new IOException("CreateEvent() failed "); - } - this.is = new NGWin32NamedPipeSocketInputStream(handle); - this.os = new NGWin32NamedPipeSocketOutputStream(handle); - } - - @Override - public InputStream getInputStream() { - return is; - } - - @Override - public OutputStream getOutputStream() { - return os; - } - - @Override - public void close() throws IOException { - closeCallback.onNamedPipeSocketClose(handle); - } - - @Override - public void shutdownInput() throws IOException { - } - - @Override - public void shutdownOutput() throws IOException { - } - - private class NGWin32NamedPipeSocketInputStream extends InputStream { - private final HANDLE handle; - - NGWin32NamedPipeSocketInputStream(HANDLE handle) { - this.handle = handle; - } - - @Override - public int read() throws IOException { - int result; - byte[] b = new byte[1]; - if (read(b) == 0) { - result = -1; - } else { - result = 0xFF & b[0]; - } - return result; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - Memory readBuffer = new Memory(len); - - WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); - olap.hEvent = readerWaitable; - olap.write(); - - boolean immediate = API.ReadFile(handle, readBuffer, len, null, olap.getPointer()); - if (!immediate) { - int lastError = API.GetLastError(); - if (lastError != WinError.ERROR_IO_PENDING) { - throw new IOException("ReadFile() failed: " + lastError); - } - } - - IntByReference read = new IntByReference(); - if (!API.GetOverlappedResult(handle, olap.getPointer(), read, true)) { - int lastError = API.GetLastError(); - throw new IOException("GetOverlappedResult() failed for read operation: " + lastError); - } - int actualLen = read.getValue(); - byte[] byteArray = readBuffer.getByteArray(0, actualLen); - System.arraycopy(byteArray, 0, b, off, actualLen); - return actualLen; - } - } - - private class NGWin32NamedPipeSocketOutputStream extends OutputStream { - private final HANDLE handle; - - NGWin32NamedPipeSocketOutputStream(HANDLE handle) { - this.handle = handle; - } - - @Override - public void write(int b) throws IOException { - write(new byte[]{(byte) (0xFF & b)}); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - ByteBuffer data = ByteBuffer.wrap(b, off, len); - - WinBase.OVERLAPPED olap = new WinBase.OVERLAPPED(); - olap.hEvent = writerWaitable; - olap.write(); - - boolean immediate = API.WriteFile(handle, data, len, null, olap.getPointer()); - if (!immediate) { - int lastError = API.GetLastError(); - if (lastError != WinError.ERROR_IO_PENDING) { - throw new IOException("WriteFile() failed: " + lastError); - } - } - IntByReference written = new IntByReference(); - if (!API.GetOverlappedResult(handle, olap.getPointer(), written, true)) { - int lastError = API.GetLastError(); - throw new IOException("GetOverlappedResult() failed for write operation: " + lastError); - } - if (written.getValue() != len) { - throw new IOException("WriteFile() wrote less bytes than requested"); - } - } - } -} diff --git a/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java b/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java deleted file mode 100644 index 7fb5d9d53..000000000 --- a/main-command/src/main/java/sbt/internal/ReferenceCountedFileDescriptor.java +++ /dev/null @@ -1,82 +0,0 @@ -// Copied from https://github.com/facebook/nailgun/blob/af623fddedfdca010df46302a0711ce0e2cc1ba6/nailgun-server/src/main/java/com/martiansoftware/nailgun/ReferenceCountedFileDescriptor.java - -/* - - Copyright 2004-2015, Martian Software, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ -package sbt.internal; - -import com.sun.jna.LastErrorException; - -import java.io.IOException; - -/** - * Encapsulates a file descriptor plus a reference count to ensure close requests - * only close the file descriptor once the last reference to the file descriptor - * is released. - * - * If not explicitly closed, the file descriptor will be closed when - * this object is finalized. - */ -public class ReferenceCountedFileDescriptor { - private int fd; - private int fdRefCount; - private boolean closePending; - - public ReferenceCountedFileDescriptor(int fd) { - this.fd = fd; - this.fdRefCount = 0; - this.closePending = false; - } - - protected void finalize() throws IOException { - close(); - } - - public synchronized int acquire() { - fdRefCount++; - return fd; - } - - public synchronized void release() throws IOException { - fdRefCount--; - if (fdRefCount == 0 && closePending && fd != -1) { - doClose(); - } - } - - public synchronized void close() throws IOException { - if (fd == -1 || closePending) { - return; - } - - if (fdRefCount == 0) { - doClose(); - } else { - // Another thread has the FD. We'll close it when they release the reference. - closePending = true; - } - } - - private void doClose() throws IOException { - try { - NGUnixDomainSocketLibrary.close(fd); - fd = -1; - } catch (LastErrorException e) { - throw new IOException(e); - } - } -} 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 8104a1736..7e31910e6 100644 --- a/main-command/src/main/scala/sbt/internal/server/Server.scala +++ b/main-command/src/main/scala/sbt/internal/server/Server.scala @@ -25,6 +25,7 @@ import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter } import sbt.internal.protocol.codec._ import sbt.internal.util.ErrorHandling import sbt.internal.util.Util.isWindows +import org.scalasbt.ipcsocket._ private[sbt] sealed trait ServerInstance { def shutdown(): Unit @@ -57,11 +58,11 @@ private[sbt] object Server { connection.connectionType match { case ConnectionType.Local if isWindows => // Named pipe already has an exclusive lock. - addServerError(new NGWin32NamedPipeServerSocket(pipeName)) + addServerError(new Win32NamedPipeServerSocket(pipeName)) case ConnectionType.Local => - tryClient(new NGUnixDomainSocket(socketfile.getAbsolutePath)) + tryClient(new UnixDomainSocket(socketfile.getAbsolutePath)) prepareSocketfile() - addServerError(new NGUnixDomainServerSocket(socketfile.getAbsolutePath)) + addServerError(new UnixDomainServerSocket(socketfile.getAbsolutePath)) case ConnectionType.Tcp => tryClient(new Socket(InetAddress.getByName(host), port)) addServerError(new ServerSocket(port, 50, InetAddress.getByName(host))) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 2ece2e12a..094491945 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -114,7 +114,7 @@ private[sbt] final class CommandExchange { subscribe(channel) } if (server.isEmpty && firstInstance.get) { - val portfile = (new File(".")).getAbsoluteFile / "project" / "target" / "active.json" + val portfile = s.baseDir / "project" / "target" / "active.json" val h = Hash.halfHashString(IO.toURI(portfile).toString) val tokenfile = BuildPaths.getGlobalBase(s) / "server" / h / "token.json" val socketfile = BuildPaths.getGlobalBase(s) / "server" / h / "sock" diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f8d470ccd..5ad8a4393 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -33,6 +33,7 @@ object Dependencies { val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.2" val rawLauncher = "org.scala-sbt" % "launcher" % "1.0.2" val testInterface = "org.scala-sbt" % "test-interface" % "1.0" + val ipcSocket = "org.scala-sbt.ipcsocket" % "ipcsocket" % "1.0.0" private val compilerInterface = "org.scala-sbt" % "compiler-interface" % zincVersion private val compilerClasspath = "org.scala-sbt" %% "zinc-classpath" % zincVersion diff --git a/protocol/src/main/scala/sbt/protocol/ClientSocket.scala b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala new file mode 100644 index 000000000..df155d439 --- /dev/null +++ b/protocol/src/main/scala/sbt/protocol/ClientSocket.scala @@ -0,0 +1,45 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt +package protocol + +import java.io.File +import java.net.{ Socket, URI, InetAddress } +import sjsonnew.BasicJsonProtocol +import sjsonnew.support.scalajson.unsafe.{ Parser, Converter } +import sjsonnew.shaded.scalajson.ast.unsafe.JValue +import sbt.internal.protocol.{ PortFile, TokenFile } +import sbt.internal.protocol.codec.{ PortFileFormats, TokenFileFormats } +import sbt.internal.util.Util.isWindows +import org.scalasbt.ipcsocket._ + +object ClientSocket { + private lazy val fileFormats = new BasicJsonProtocol with PortFileFormats with TokenFileFormats {} + + def socket(portfile: File): (Socket, Option[String]) = { + import fileFormats._ + val json: JValue = Parser.parseFromFile(portfile).get + val p = Converter.fromJson[PortFile](json).get + 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 + } + val sk = uri.getScheme match { + case "local" if isWindows => + new Win32NamedPipeSocket("""\\.\pipe\""" + uri.getSchemeSpecificPart) + case "local" => new UnixDomainSocket(uri.getSchemeSpecificPart) + case "tcp" => new Socket(InetAddress.getByName(uri.getHost), uri.getPort) + case _ => sys.error(s"Unsupported uri: $uri") + } + (sk, token) + } +} diff --git a/sbt/src/sbt-test/server/handshake/Client.scala b/sbt/src/sbt-test/server/handshake/Client.scala deleted file mode 100644 index 2f41f4c18..000000000 --- a/sbt/src/sbt-test/server/handshake/Client.scala +++ /dev/null @@ -1,106 +0,0 @@ -package example - -import java.net.{ URI, Socket, InetAddress, SocketException } -import sbt.io._ -import sbt.io.syntax._ -import java.io.File -import sjsonnew.support.scalajson.unsafe.{ Parser, Converter, CompactPrinter } -import sjsonnew.shaded.scalajson.ast.unsafe.{ JValue, JObject, JString } - -object Client extends App { - val host = "127.0.0.1" - val delimiter: Byte = '\n'.toByte - - lazy val connection = getConnection - lazy val out = connection.getOutputStream - lazy val in = connection.getInputStream - - val t = getToken - val msg0 = s"""{ "type": "InitCommand", "token": "$t" }""" - - writeLine(s"Content-Length: ${ msg0.size + 2 }") - writeLine("Content-Type: application/sbt-x1") - writeLine("") - writeLine(msg0) - out.flush - - writeLine("Content-Length: 49") - writeLine("Content-Type: application/sbt-x1") - writeLine("") - // 12345678901234567890123456789012345678901234567890 - writeLine("""{ "type": "ExecCommand", "commandLine": "exit" }""") - writeLine("") - out.flush - - val baseDirectory = new File(args(0)) - IO.write(baseDirectory / "ok.txt", "ok") - - def getToken: String = { - val tokenfile = new File(getTokenFileUri) - val json: JValue = Parser.parseFromFile(tokenfile).get - json match { - case JObject(fields) => - (fields find { _.field == "token" } map { _.value }) match { - case Some(JString(value)) => value - case _ => - sys.error("json doesn't token field that is JString") - } - case _ => sys.error("json doesn't have token field") - } - } - - def getTokenFileUri: URI = { - val portfile = baseDirectory / "project" / "target" / "active.json" - val json: JValue = Parser.parseFromFile(portfile).get - json match { - case JObject(fields) => - (fields find { _.field == "tokenfileUri" } map { _.value }) match { - case Some(JString(value)) => new URI(value) - case _ => - sys.error("json doesn't tokenfile field that is JString") - } - case _ => sys.error("json doesn't have tokenfile field") - } - } - - def getPort: Int = { - val portfile = baseDirectory / "project" / "target" / "active.json" - val json: JValue = Parser.parseFromFile(portfile).get - json match { - case JObject(fields) => - (fields find { _.field == "uri" } map { _.value }) match { - case Some(JString(value)) => - val u = new URI(value) - u.getPort - case _ => - sys.error("json doesn't uri field that is JString") - } - case _ => sys.error("json doesn't have uri field") - } - } - - def getConnection: Socket = - try { - new Socket(InetAddress.getByName(host), getPort) - } catch { - case _ => - Thread.sleep(1000) - getConnection - } - - def writeLine(s: String): Unit = { - if (s != "") { - out.write(s.getBytes("UTF-8")) - } - writeEndLine - } - - def writeEndLine(): Unit = { - val retByte: Byte = '\r'.toByte - val delimiter: Byte = '\n'.toByte - - out.write(retByte.toInt) - out.write(delimiter.toInt) - out.flush - } -} diff --git a/sbt/src/sbt-test/server/handshake/build.sbt b/sbt/src/sbt-test/server/handshake/build.sbt deleted file mode 100644 index 851648f3c..000000000 --- a/sbt/src/sbt-test/server/handshake/build.sbt +++ /dev/null @@ -1,15 +0,0 @@ -lazy val runClient = taskKey[Unit]("") - -lazy val root = (project in file(".")) - .settings( - serverConnectionType in Global := ConnectionType.Tcp, - scalaVersion := "2.12.3", - serverPort in Global := 5123, - libraryDependencies += "org.scala-sbt" %% "io" % "1.0.1", - libraryDependencies += "com.eed3si9n" %% "sjson-new-scalajson" % "0.8.0", - runClient := (Def.taskDyn { - val b = baseDirectory.value - (bgRun in Compile).toTask(s""" $b""") - }).value - ) - \ No newline at end of file diff --git a/sbt/src/sbt-test/server/handshake/test b/sbt/src/sbt-test/server/handshake/test deleted file mode 100644 index 703942376..000000000 --- a/sbt/src/sbt-test/server/handshake/test +++ /dev/null @@ -1,6 +0,0 @@ -> show serverPort -> runClient - --> shell - -$ exists ok.txt diff --git a/sbt/src/server-test/handshake/build.sbt b/sbt/src/server-test/handshake/build.sbt new file mode 100644 index 000000000..192730eef --- /dev/null +++ b/sbt/src/server-test/handshake/build.sbt @@ -0,0 +1,6 @@ +lazy val root = (project in file(".")) + .settings( + Global / serverLog / logLevel := Level.Debug, + name := "handshake", + scalaVersion := "2.12.3", + ) diff --git a/sbt/src/test/scala/sbt/RunFromSourceMain.scala b/sbt/src/test/scala/sbt/RunFromSourceMain.scala index bd2f142f7..b4fc8bce4 100644 --- a/sbt/src/test/scala/sbt/RunFromSourceMain.scala +++ b/sbt/src/test/scala/sbt/RunFromSourceMain.scala @@ -22,7 +22,7 @@ object RunFromSourceMain { // this arrangement is because Scala does not always properly optimize away // the tail recursion in a catch statement - @tailrec private def run(baseDir: File, args: Seq[String]): Unit = + @tailrec private[sbt] def run(baseDir: File, args: Seq[String]): Unit = runImpl(baseDir, args) match { case Some((baseDir, args)) => run(baseDir, args) case None => () diff --git a/sbt/src/test/scala/sbt/ServerSpec.scala b/sbt/src/test/scala/sbt/ServerSpec.scala new file mode 100644 index 000000000..648d88203 --- /dev/null +++ b/sbt/src/test/scala/sbt/ServerSpec.scala @@ -0,0 +1,179 @@ +/* + * sbt + * Copyright 2011 - 2017, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under BSD-3-Clause license (see LICENSE) + */ + +package sbt + +import org.scalatest._ +import scala.concurrent._ +import java.io.{ InputStream, OutputStream } +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.{ ThreadFactory, ThreadPoolExecutor } +import sbt.protocol.ClientSocket + +class ServerSpec extends AsyncFlatSpec with Matchers { + import ServerSpec._ + + "server" should "start" in { + withBuildSocket("handshake") { (out, in, tkn) => + writeLine( + """{ "jsonrpc": "2.0", "id": 3, "method": "sbt/setting", "params": { "setting": "root/name" } }""", + out) + Thread.sleep(100) + val l2 = contentLength(in) + println(l2) + readLine(in) + readLine(in) + val x2 = readContentLength(in, l2) + println(x2) + assert(1 == 1) + } + } +} + +object ServerSpec { + private val serverTestBase: File = new File(".").getAbsoluteFile / "sbt" / "src" / "server-test" + private val nextThreadId = new AtomicInteger(1) + private val threadGroup = Thread.currentThread.getThreadGroup() + val readBuffer = new Array[Byte](4096) + var buffer: Vector[Byte] = Vector.empty + var bytesRead = 0 + private val delimiter: Byte = '\n'.toByte + private val RetByte = '\r'.toByte + + private val threadFactory = new ThreadFactory() { + override def newThread(runnable: Runnable): Thread = { + val thread = + new Thread(threadGroup, + runnable, + s"sbt-test-server-threads-${nextThreadId.getAndIncrement}") + // Do NOT setDaemon because then the code in TaskExit.scala in sbt will insta-kill + // the backgrounded process, at least for the case of the run task. + thread + } + } + + private val executor = new ThreadPoolExecutor( + 0, /* corePoolSize */ + 1, /* maxPoolSize, max # of servers */ + 2, + java.util.concurrent.TimeUnit.SECONDS, + /* keep alive unused threads this long (if corePoolSize < maxPoolSize) */ + new java.util.concurrent.SynchronousQueue[Runnable](), + threadFactory + ) + + def backgroundRun(baseDir: File, args: Seq[String]): Unit = { + executor.execute(new Runnable { + def run(): Unit = { + RunFromSourceMain.run(baseDir, args) + } + }) + } + + def shutdown(): Unit = executor.shutdown() + + def withBuildSocket(testBuild: String)( + f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = { + IO.withTemporaryDirectory { temp => + IO.copyDirectory(serverTestBase / testBuild, temp / testBuild) + withBuildSocket(temp / testBuild)(f) + } + } + + def sendJsonRpc(message: String, out: OutputStream): Unit = { + writeLine(s"""Content-Length: ${message.size + 2}""", out) + writeLine("", out) + writeLine(message, out) + } + + def contentLength(in: InputStream): Int = { + readLine(in) map { line => + line.drop(16).toInt + } getOrElse (0) + } + + def readLine(in: InputStream): Option[String] = { + if (buffer.isEmpty) { + val bytesRead = in.read(readBuffer) + if (bytesRead > 0) { + buffer = buffer ++ readBuffer.toVector.take(bytesRead) + } + } + val delimPos = buffer.indexOf(delimiter) + if (delimPos > 0) { + val chunk0 = buffer.take(delimPos) + buffer = buffer.drop(delimPos + 1) + // remove \r at the end of line. + if (chunk0.size > 0 && chunk0.indexOf(RetByte) == chunk0.size - 1) + Some(new String(chunk0.dropRight(1).toArray, "utf-8")) + else Some(new String(chunk0.toArray, "utf-8")) + } else None // no EOL yet, so skip this turn. + } + + def readContentLength(in: InputStream, length: Int): Option[String] = { + if (buffer.isEmpty) { + val bytesRead = in.read(readBuffer) + if (bytesRead > 0) { + buffer = buffer ++ readBuffer.toVector.take(bytesRead) + } + } + if (length <= buffer.size) { + val chunk = buffer.take(length) + buffer = buffer.drop(length) + Some(new String(chunk.toArray, "utf-8")) + } else None // have not read enough yet, so skip this turn. + } + + def writeLine(s: String, out: OutputStream): Unit = { + def writeEndLine(): Unit = { + val retByte: Byte = '\r'.toByte + val delimiter: Byte = '\n'.toByte + out.write(retByte.toInt) + out.write(delimiter.toInt) + out.flush + } + + if (s != "") { + out.write(s.getBytes("UTF-8")) + } + writeEndLine + } + + def withBuildSocket(baseDirectory: File)( + f: (OutputStream, InputStream, Option[String]) => Future[Assertion]): Future[Assertion] = { + backgroundRun(baseDirectory, Nil) + + val portfile = baseDirectory / "project" / "target" / "active.json" + + def waitForPortfile(n: Int): Unit = + if (portfile.exists) () + else { + if (n <= 0) sys.error(s"Timeout. $portfile is not found.") + else { + Thread.sleep(1000) + waitForPortfile(n - 1) + } + } + waitForPortfile(10) + val (sk, tkn) = ClientSocket.socket(portfile) + val out = sk.getOutputStream + val in = sk.getInputStream + + sendJsonRpc( + """{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { } } }""", + out) + + try { + f(out, in, tkn) + } finally { + sendJsonRpc( + """{ "jsonrpc": "2.0", "id": 9, "method": "sbt/exec", "params": { "commandLine": "exit" } }""", + out) + shutdown() + } + } +}