From f79e9bbad7aefd9aeea89fd4c456b1eb447917db Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 9 Apr 2018 22:16:27 -0700 Subject: - Swap client-server integer encoding over to a more standard format (32-bit) - Unit tests for client code using the new Java support - Make server auto-shutdown when the client version changes, to avoid stale-server confusion --- client/src/mill/client/ClientServer.java | 97 ---------------------------- client/src/mill/client/Main.java | 18 +++--- client/src/mill/client/Util.java | 95 +++++++++++++++++++++++++++ client/test/src/mill/client/ClientTests.java | 61 +++++++++++++++++ 4 files changed, 166 insertions(+), 105 deletions(-) delete mode 100644 client/src/mill/client/ClientServer.java create mode 100644 client/src/mill/client/Util.java create mode 100644 client/test/src/mill/client/ClientTests.java (limited to 'client') diff --git a/client/src/mill/client/ClientServer.java b/client/src/mill/client/ClientServer.java deleted file mode 100644 index 468f8ab3..00000000 --- a/client/src/mill/client/ClientServer.java +++ /dev/null @@ -1,97 +0,0 @@ -package mill.client; - - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; - -public class ClientServer { - public static boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); - public static boolean isJava9OrAbove = !System.getProperty("java.specification.version").startsWith("1."); - - // Windows named pipe prefix (see https://github.com/sbt/ipcsocket/blob/v1.0.0/README.md) - // Win32NamedPipeServerSocket automatically adds this as a prefix (if it is not already is prefixed), - // but Win32NamedPipeSocket does not - // https://github.com/sbt/ipcsocket/blob/v1.0.0/src/main/java/org/scalasbt/ipcsocket/Win32NamedPipeServerSocket.java#L36 - public static String WIN32_PIPE_PREFIX = "\\\\.\\pipe\\"; - - public static String[] parseArgs(InputStream argStream) throws IOException { - - int argsLength = argStream.read(); - String[] args = new String[argsLength]; - for (int i = 0; i < args.length; i++) { - args[i] = readString(argStream); - } - return args; - } - public static void writeArgs(Boolean interactive, - String[] args, - OutputStream argStream) throws IOException { - argStream.write(interactive ? 1 : 0); - argStream.write(args.length); - int i = 0; - while (i < args.length) { - writeString(argStream, args[i]); - i += 1; - } - } - - /** - * This allows the mill client to pass the environment as he sees it to the - * server (as the server remains alive over the course of several runs and - * does not see the environment changes the client would) - */ - public static void writeMap(Map map, OutputStream argStream) throws IOException { - argStream.write(map.size()); - for (Map.Entry kv : map.entrySet()) { - writeString(argStream, kv.getKey()); - writeString(argStream, kv.getValue()); - } - } - - public static Map parseMap(InputStream argStream) throws IOException { - Map env = new HashMap<>(); - int mapLength = argStream.read(); - for (int i = 0; i < mapLength; i++) { - String key = readString(argStream); - String value = readString(argStream); - env.put(key, value); - } - return env; - } - - private static String readString(InputStream inputStream) throws IOException { - // Result is between 0 and 255, hence the loop. - int read = inputStream.read(); - int bytesToRead = read; - while(read == 255){ - read = inputStream.read(); - bytesToRead += read; - } - byte[] arr = new byte[bytesToRead]; - int readTotal = 0; - while (readTotal < bytesToRead) { - read = inputStream.read(arr, readTotal, bytesToRead - readTotal); - readTotal += read; - } - return new String(arr); - } - - private static void writeString(OutputStream outputStream, String string) throws IOException { - // When written, an int > 255 gets splitted. This logic performs the - // split beforehand so that the reading side knows that there is still - // more metadata to come before it's able to read the actual data. - // Could do with rewriting using logical masks / shifts. - byte[] bytes = string.getBytes(); - int toWrite = bytes.length; - while(toWrite >= 255){ - outputStream.write(255); - toWrite = toWrite - 255; - } - outputStream.write(toWrite); - outputStream.write(bytes); - } - -} diff --git a/client/src/mill/client/Main.java b/client/src/mill/client/Main.java index 109a9a9d..3a0a2db8 100644 --- a/client/src/mill/client/Main.java +++ b/client/src/mill/client/Main.java @@ -22,7 +22,7 @@ public class Main { } current = current.getParent(); } - if (ClientServer.isJava9OrAbove) { + if (Util.isJava9OrAbove) { selfJars.addAll(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator))); } ArrayList l = new java.util.ArrayList(); @@ -90,7 +90,6 @@ public class Main { throw new Exception("Reached max process limit: " + 5); } - public static int run(String lockBase, Runnable initServer, Locks locks, @@ -101,8 +100,10 @@ public class Main { Map env) throws Exception{ FileOutputStream f = new FileOutputStream(lockBase + "/run"); - ClientServer.writeArgs(System.console() != null, args, f); - ClientServer.writeMap(env, f); + f.write(System.console() != null ? 1 : 0); + Util.writeString(f, System.getProperty("MILL_VERSION")); + Util.writeArgs(args, f); + Util.writeMap(env, f); f.close(); boolean serverInit = false; @@ -114,15 +115,15 @@ public class Main { // Need to give sometime for Win32NamedPipeSocket to work // if the server is just initialized - if (serverInit && ClientServer.isWindows) Thread.sleep(1000); + if (serverInit && Util.isWindows) Thread.sleep(1000); Socket ioSocket = null; long retryStart = System.currentTimeMillis(); while(ioSocket == null && System.currentTimeMillis() - retryStart < 1000){ try{ - ioSocket = ClientServer.isWindows? - new Win32NamedPipeSocket(ClientServer.WIN32_PIPE_PREFIX + new File(lockBase).getName()) + ioSocket = Util.isWindows? + new Win32NamedPipeSocket(Util.WIN32_PIPE_PREFIX + new File(lockBase).getName()) : new UnixDomainSocket(lockBase + "/io"); }catch(Throwable e){ Thread.sleep(1); @@ -131,6 +132,7 @@ public class Main { if (ioSocket == null){ throw new Exception("Failed to connect to server"); } + InputStream outErr = ioSocket.getInputStream(); OutputStream in = ioSocket.getOutputStream(); ClientOutputPumper outPump = new ClientOutputPumper(outErr, stdout, stderr); @@ -205,7 +207,7 @@ class ClientOutputPumper implements Runnable{ // instead it throws an IOException whose message contains "ReadFile()". // However, if it throws an IOException before ever reading some bytes, // it could not connect to the server, so exit. - if (ClientServer.isWindows && e.getMessage().contains("ReadFile()")) { + if (Util.isWindows && e.getMessage().contains("ReadFile()")) { if (first) { System.err.println("Failed to connect to server"); System.exit(1); diff --git a/client/src/mill/client/Util.java b/client/src/mill/client/Util.java new file mode 100644 index 00000000..f6adb3cc --- /dev/null +++ b/client/src/mill/client/Util.java @@ -0,0 +1,95 @@ +package mill.client; + + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +public class Util { + public static boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows"); + public static boolean isJava9OrAbove = !System.getProperty("java.specification.version").startsWith("1."); + + // Windows named pipe prefix (see https://github.com/sbt/ipcsocket/blob/v1.0.0/README.md) + // Win32NamedPipeServerSocket automatically adds this as a prefix (if it is not already is prefixed), + // but Win32NamedPipeSocket does not + // https://github.com/sbt/ipcsocket/blob/v1.0.0/src/main/java/org/scalasbt/ipcsocket/Win32NamedPipeServerSocket.java#L36 + public static String WIN32_PIPE_PREFIX = "\\\\.\\pipe\\"; + + public static String[] parseArgs(InputStream argStream) throws IOException { + + int argsLength = readInt(argStream); + String[] args = new String[argsLength]; + for (int i = 0; i < args.length; i++) { + args[i] = readString(argStream); + } + return args; + } + public static void writeArgs(String[] args, + OutputStream argStream) throws IOException { + writeInt(argStream, args.length); + for(String arg: args){ + writeString(argStream, arg); + } + } + + /** + * This allows the mill client to pass the environment as he sees it to the + * server (as the server remains alive over the course of several runs and + * does not see the environment changes the client would) + */ + public static void writeMap(Map map, OutputStream argStream) throws IOException { + writeInt(argStream, map.size()); + for (Map.Entry kv : map.entrySet()) { + writeString(argStream, kv.getKey()); + writeString(argStream, kv.getValue()); + } + } + + public static Map parseMap(InputStream argStream) throws IOException { + Map env = new HashMap<>(); + int mapLength = readInt(argStream); + for (int i = 0; i < mapLength; i++) { + String key = readString(argStream); + String value = readString(argStream); + env.put(key, value); + } + return env; + } + + public static String readString(InputStream inputStream) throws IOException { + // Result is between 0 and 255, hence the loop. + int length = readInt(inputStream); + byte[] arr = new byte[length]; + int total = 0; + while(total < length){ + int res = inputStream.read(arr, total, length-total); + if (res == -1) throw new IOException("Incomplete String"); + else{ + total += res; + } + } + return new String(arr); + } + + public static void writeString(OutputStream outputStream, String string) throws IOException { + byte[] bytes = string.getBytes(); + writeInt(outputStream, bytes.length); + outputStream.write(bytes); + } + + public static void writeInt(OutputStream out, int i) throws IOException{ + out.write((byte)(i >>> 24)); + out.write((byte)(i >>> 16)); + out.write((byte)(i >>> 8)); + out.write((byte)i); + } + public static int readInt(InputStream in) throws IOException{ + return ((in.read() & 0xFF) << 24) + + ((in.read() & 0xFF) << 16) + + ((in.read() & 0xFF) << 8) + + (in.read() & 0xFF); + } + +} diff --git a/client/test/src/mill/client/ClientTests.java b/client/test/src/mill/client/ClientTests.java new file mode 100644 index 00000000..4059e5c7 --- /dev/null +++ b/client/test/src/mill/client/ClientTests.java @@ -0,0 +1,61 @@ +package mill.client; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +public class ClientTests { + @Test + public void readWriteInt() throws Exception{ + int[] examples = { + 0, 1, 126, 127, 128, 254, 255, 256, 1024, 99999, 1234567, + Integer.MAX_VALUE, Integer.MAX_VALUE / 2, Integer.MIN_VALUE + }; + for(int example0: examples){ + for(int example: new int[]{-example0, example0}){ + ByteArrayOutputStream o = new ByteArrayOutputStream(); + Util.writeInt(o, example); + ByteArrayInputStream i = new ByteArrayInputStream(o.toByteArray()); + int s = Util.readInt(i); + assertEquals(example, s); + assertEquals(i.available(), 0); + } + } + } + @Test + public void readWriteString() throws Exception{ + String[] examples = { + "", + "hello", + "i am cow", + "i am cow\nhear me moo\ni weight twice as much as you", + "我是一个叉烧包", + }; + for(String example: examples){ + checkStringRoundTrip(example); + } + } + + @Test + public void readWriteBigString() throws Exception{ + int[] lengths = {0, 1, 126, 127, 128, 254, 255, 256, 1024, 99999, 1234567}; + for(int i = 0; i < lengths.length; i++){ + final char[] bigChars = new char[lengths[i]]; + java.util.Arrays.fill(bigChars, 'X'); + checkStringRoundTrip(new String(bigChars)); + } + } + + public void checkStringRoundTrip(String example) throws Exception{ + ByteArrayOutputStream o = new ByteArrayOutputStream(); + Util.writeString(o, example); + ByteArrayInputStream i = new ByteArrayInputStream(o.toByteArray()); + String s = Util.readString(i); + assertEquals(example, s); + assertEquals(i.available(), 0); + } + + +} \ No newline at end of file -- cgit v1.2.3