diff --git a/README.md b/README.md index 82418f7..397d1c3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Scalive + This tool allows you to connect a Scala REPL console to running Oracle (Sun) JVM processes without any prior setup at the target process. @@ -5,56 +7,55 @@ JVM processes without any prior setup at the target process. ## Download -Download and extract +For Scala 2.12, download +[scalive-1.7.0.zip](https://github.com/xitrum-framework/scalive/releases/download/1.7.0/scalive-1.7.0.zip). + +For Scala 2.10 and 2.11, download [scalive-1.6.zip](https://github.com/xitrum-framework/scalive/releases/download/1.6/scalive-1.6.zip), -you will see: -``` -scalive-1.6/ - scalive - scalive.cmd - scalive-1.6.jar - jline-2.14.2.jar +Extract the ZIP file, you will see: - scala-library-2.10.6.jar - scala-compiler-2.10.6.jar - scala-reflect-2.10.6.jar +```txt +scalive-1.7.0/ + scalive + scalive.bat + scalive-1.7.0.jar - scala-library-2.11.8.jar - scala-compiler-2.11.8.jar - scala-reflect-2.11.8.jar + scala-library-2.12.8.jar + scala-compiler-2.12.8.jar + scala-reflect-2.12.8.jar ``` scala-library, scala-compiler, and scala-reflect of the correct version -that your JVM process is using will be loaded, if they have not been loaded. +that your JVM process is using will be loaded, if they have not been loaded yet. The REPL console needs these libraries to work. -For example, your process has already loaded scala-library 2.11.8 by itself, +For example, your process has already loaded scala-library 2.12.8 by itself, but scala-compiler and scala-reflect haven't been loaded, Scalive will -automatically load their version 2.11.8. +automatically load their version 2.12.8. If none of them has been loaded, i.e. your process doesn't use Scala, Scalive will load the lastest version in the directory. -For your convenience, Scala 2.10.6 and 2.11.8 JARs are included above. +For your convenience, Scala 2.12.8 JAR files have been included above. -If your process is using a different Scala version, you need to manually +If your process uses a different Scala version, you need to manually download the corresponding JARs from the Internet and save them in the same directory as above. ## Usage -Run the shell script `scalive` (*nix) or `scalive.cmd` (Windows). +Run the shell script `scalive` (*nix) or `scalive.bat` (Windows). Run without argument to see the list of running JVM process IDs on your local machine: -``` +```sh scalive ``` Example output: -``` +```txt JVM processes: #pid Display name 13821 demos.Boot @@ -63,7 +64,7 @@ JVM processes: To connect a Scala REPL console to a process: -``` +```sh scalive ``` @@ -80,7 +81,7 @@ Scalive only automatically loads `scala-library.jar`, `scala-compiler.jar`, If you want to load additional classes in other JARs, first run these in the REPL console to load the JAR to the system class loader: -``` +```scala val cl = ClassLoader.getSystemClassLoader.asInstanceOf[java.net.URLClassLoader] val jarSearchDirs = Array("/dir/containing/the/jar") val jarPrefix = "mylib" // Will match "mylib-xxx.jar", convenient when there's version number in the file name @@ -90,7 +91,7 @@ scalive.Classpath.findAndAddJar(cl, jarSearchDirs, jarPrefix) Now the trick is just quit the REPL console and connect it to the target process again. You will be able to use your classes in the JAR normally: -``` +```scala import mylib.foo.Bar ... ``` @@ -99,13 +100,13 @@ import mylib.foo.Bar ## How Scalive works -Scalive uses the [Attach API](https://blogs.oracle.com/CoreJavaTechTips/entry/the_attach_api) in Java 6 +Scalive uses the [Attach API](https://blogs.oracle.com/CoreJavaTechTips/entry/the_attach_api) to tell the target process to load an [agent](http://javahowto.blogspot.jp/2006/07/javaagent-option.html). Inside the target progress, the agent creates a REPL interpreter and a TCP server to let the Scalive process connect and interact with the interpreter. The Scalive process acts as a TCP client. There are 2 TCP -connections, one for REPL data and one for completion data. +connections, one for REPL data and one for tab key completion data. Similar projects: @@ -118,7 +119,7 @@ For simplicity and to avoid memory leak when you attach/detach many times, Scalive only supports processes with only the default system class loader, without additional class loaders. Usually they are standalone JVM processes, like -[Play](http://www.playframework.com/) or -[Xitrum](http://xitrum-framework.github.io/) in production mode. +[Play](http://www.playframework.com) or +[Xitrum](http://xitrum-framework.github.io) in production mode. Processes with multiple class loaders like Tomcat are currently not supported. diff --git a/build.sbt b/build.sbt index 45cc2f3..58d8111 100755 --- a/build.sbt +++ b/build.sbt @@ -1,19 +1,20 @@ organization := "tv.cntt" name := "scalive" -version := "1.6" +version := "1.7.0" -scalaVersion := "2.11.8" +// Scalive is a Java library, we only use SBT to build +scalaVersion := "2.12.8" autoScalaLibrary := false crossPaths := false // Do not append Scala versions to the generated artifacts javacOptions ++= Seq("-Xlint:deprecation") -// Ensure Scalive can run on Java 6 -scalacOptions += "-target:jvm-1.6" -javacOptions ++= Seq("-source", "1.6", "-target", "1.6") +// Ensure Scalive can run on Java from 8 (Scala 2.12 requires Java 8) +scalacOptions += "-target:jvm-1.8" +javacOptions ++= Seq("-source", "1.8", "-target", "1.8") +// scala-compiler already embeds JLine, no need to add JLine dependency separately libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value % "provided" -libraryDependencies += "jline" % "jline" % "2.14.2" // Add tools.jar to classpath // https://blogs.oracle.com/CoreJavaTechTips/entry/the_attach_api diff --git a/dev/README.rst b/dev/README.rst index 61f6a88..bb14027 100644 --- a/dev/README.rst +++ b/dev/README.rst @@ -24,17 +24,12 @@ This is the directory that will be zipped when Scalive is released. zip/ scalive - scalive.cmd + scalive.bat scalive-.jar <- ../../target/scala-2.11/scalive-.jar - jline-2.14.2.jar <- Download and put JLine JAR here - scala-library-2.10.6.jar - scala-compiler-2.10..jar - scala-reflect-2.10.6.jar - - scala-library-2.11.8.jar - scala-compiler-2.11.8.jar - scala-reflect-2.11.8.jar + scala-library-2.12.8.jar + scala-compiler-2.12.8.jar + scala-reflect-2.12.8.jar While developing: @@ -46,22 +41,20 @@ Release ------- Based on the ``zip`` directory above, prepare a directory to be zipped and -released (remember to remove uneccessary files, like .gitignore): +released (remember to remove unneccessary files, like .gitignore): :: scalive-/ scalive - scalive.cmd + scalive.bat scalive-.jar <- Doesn't depend on Scala, thus doesn't follow Scala JAR naming - scala-library-2.10.4.jar - scala-compiler-2.10.4.jar - scala-reflect-2.10.4.jar + scala-library-2.12.8.jar + scala-compiler-2.12.8.jar + scala-reflect-2.12.8.jar - scala-library-2.11.4.jar - scala-compiler-2.11.4.jar - scala-reflect-2.11.4.jar + README.md Then zip it: diff --git a/dev/zip/scalive.cmd b/dev/zip/scalive.bat similarity index 100% rename from dev/zip/scalive.cmd rename to dev/zip/scalive.bat diff --git a/project/build.properties b/project/build.properties index 35c88ba..c0bab04 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.12 +sbt.version=1.2.8 diff --git a/project/plugins.sbt b/project/plugins.sbt index 15f46b2..bb3a81d 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,2 @@ -// Run sbt eclipse to create Eclipse project file -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") +// Run "sbt eclipse" to create Eclipse project file +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") diff --git a/src/main/java/scalive/Classpath.java b/src/main/java/scalive/Classpath.java index 4fbe1e6..ef66e36 100644 --- a/src/main/java/scalive/Classpath.java +++ b/src/main/java/scalive/Classpath.java @@ -7,6 +7,8 @@ import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; public class Classpath { private static final Method addURL = getAddURL(); @@ -51,7 +53,7 @@ public static String findJar(String[] jarSearchDirs, String jarPrefix) throws Il } if (maxFile == null) - throw new IllegalStateException("Could not find " + jarPrefix + " in " + join(jarSearchDirs, File.pathSeparator)); + throw new IllegalStateException("Could not find " + jarPrefix + " in " + String.join(File.pathSeparator, jarSearchDirs)); else return maxFile.getPath(); } @@ -78,7 +80,7 @@ public static void findAndAddJar( * but only find and add the JAR to classpath if the representativeClass has not been loaded. */ public static void findAndAddJar( - URLClassLoader cl, String representativeClass, String[] jarSearchDirs, String jarPrefix + URLClassLoader cl, String representativeClass, String[] jarSearchDirs, String jarPrefix ) throws IllegalAccessException, MalformedURLException, InvocationTargetException { try { Class.forName(representativeClass, true, cl); @@ -90,7 +92,7 @@ public static void findAndAddJar( // http://stackoverflow.com/questions/4121567/embedded-scala-repl-inherits-parent-classpath public static String getClasspath(URLClassLoader cl) { URL[] urls = cl.getURLs(); - return join(urls, File.pathSeparator); + return Arrays.stream(urls).map(Objects::toString).collect(Collectors.joining(File.pathSeparator)); } public static String getScalaVersion( @@ -100,15 +102,4 @@ public static String getScalaVersion( Method m = k.getDeclaredMethod("versionNumberString"); return (String) m.invoke(k); } - - //-------------------------------------------------------------------------- - - private static String join(Object[] xs, String separator) { - StringBuilder b = new StringBuilder(); - for (Object x: xs) { - if (b.length() > 0) b.append(separator); - b.append(x); - } - return b.toString(); - } } diff --git a/src/main/java/scalive/Net.java b/src/main/java/scalive/Net.java index 2ec173b..0fcb4a8 100644 --- a/src/main/java/scalive/Net.java +++ b/src/main/java/scalive/Net.java @@ -36,15 +36,12 @@ public static void throwSocketTimeoutExceptionForLongInactivity(Socket socket) t * The sockets are closed in the order they are given. */ public static Runnable getSocketCleaner(final Socket... sockets) { - return new Runnable() { - @Override - public void run() { - for (Socket socket : sockets) { - try { - socket.close(); - } catch (IOException e) { - // Ignore - } + return () -> { + for (Socket socket : sockets) { + try { + socket.close(); + } catch (IOException e) { + // Ignore } } }; diff --git a/src/main/java/scalive/client/Client.java b/src/main/java/scalive/client/Client.java index cbf201c..41a06d5 100644 --- a/src/main/java/scalive/client/Client.java +++ b/src/main/java/scalive/client/Client.java @@ -1,6 +1,6 @@ package scalive.client; -import jline.console.ConsoleReader; +import scala.tools.jline_embedded.console.ConsoleReader; import scalive.Log; import scalive.Net; @@ -23,7 +23,7 @@ public void run() { }); ConsoleReader reader = new ConsoleReader(System.in, System.out); - Completer.setup(completerSocket, reader); - Repl.run(replSocket, reader); + ClientCompleter.setup(completerSocket, reader); + ClientRepl.run(replSocket, reader); } } diff --git a/src/main/java/scalive/client/ClientCompleter.java b/src/main/java/scalive/client/ClientCompleter.java new file mode 100644 index 0000000..e7d8e9c --- /dev/null +++ b/src/main/java/scalive/client/ClientCompleter.java @@ -0,0 +1,51 @@ +package scalive.client; + +import scala.tools.jline_embedded.console.ConsoleReader; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * Input: cursor buffer + * + * Output: cursor candidate1 candidate2 candidate3... + */ +class ClientCompleter { + static void setup(Socket socket, ConsoleReader reader) throws Exception { + final InputStream in = socket.getInputStream(); + final OutputStream out = socket.getOutputStream(); + + final BufferedReader b = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + + reader.addCompleter((String buffer, int cursor, List candidates) -> { + try { + String request = String.format("%d %s\n", cursor, buffer); + out.write(request.getBytes(StandardCharsets.UTF_8)); + out.flush(); + + String response = b.readLine(); + + // socket closed; the client should exit + if (response == null) return -1; + + String[] args = response.split(" "); + + int c = Integer.parseInt(args[0]); + + for (int i = 1; i < args.length; i++) { + String candidate = args[i]; + candidates.add(candidate); + } + + return c; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/src/main/java/scalive/client/Repl.java b/src/main/java/scalive/client/ClientRepl.java similarity index 76% rename from src/main/java/scalive/client/Repl.java rename to src/main/java/scalive/client/ClientRepl.java index a839cc3..ab4604d 100644 --- a/src/main/java/scalive/client/Repl.java +++ b/src/main/java/scalive/client/ClientRepl.java @@ -1,20 +1,20 @@ package scalive.client; -import jline.console.ConsoleReader; +import scala.tools.jline_embedded.console.ConsoleReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.Socket; +import java.nio.charset.StandardCharsets; -class Repl { +class ClientRepl { static void run(Socket socket, final ConsoleReader reader) throws IOException { final InputStream in = socket.getInputStream(); final OutputStream out = socket.getOutputStream(); - new Thread(Repl.class.getName() + "-printServerOutput") { + new Thread(ClientRepl.class.getName() + "-printServerOutput") { @Override public void run() { try { @@ -29,9 +29,6 @@ public void run() { } private static void readLocalInput(ConsoleReader reader, OutputStream out) throws IOException { - // Need to set, even to empty, otherwise JLine doesn't work well - reader.setPrompt(""); - while (true) { // Read String line = reader.readLine(); @@ -39,7 +36,7 @@ private static void readLocalInput(ConsoleReader reader, OutputStream out) throw // Evaluate try { - out.write(line.getBytes("UTF-8")); + out.write(line.getBytes(StandardCharsets.UTF_8)); out.write('\n'); out.flush(); } catch (IOException e) { @@ -49,8 +46,8 @@ private static void readLocalInput(ConsoleReader reader, OutputStream out) throw } } - private static void printServerOutput(InputStream in) throws UnsupportedEncodingException { - InputStreamReader reader = new InputStreamReader(in, "UTF-8"); + private static void printServerOutput(InputStream in) { + InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8); while (true) { int i; try { diff --git a/src/main/java/scalive/client/Completer.java b/src/main/java/scalive/client/Completer.java deleted file mode 100644 index 6740546..0000000 --- a/src/main/java/scalive/client/Completer.java +++ /dev/null @@ -1,53 +0,0 @@ -package scalive.client; - -import jline.console.ConsoleReader; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.Socket; -import java.util.List; - -/** - * Input: cursor buffer - * - * Output: cursor candidate1 candidate2 candidate3... - */ -class Completer { - static void setup(Socket socket, ConsoleReader reader) throws Exception { - final InputStream in = socket.getInputStream(); - final OutputStream out = socket.getOutputStream(); - - final BufferedReader b = new BufferedReader(new InputStreamReader(in, "UTF-8")); - - reader.addCompleter(new jline.console.completer.Completer() { - @Override - public int complete(String buffer, int cursor, List candidates) { - try { - String request = String.format("%d %s\n", cursor, buffer); - out.write(request.getBytes("UTF-8")); - out.flush(); - - String response = b.readLine(); - - // socket closed; the client should exit - if (response == null) return -1; - - String[] args = response.split(" "); - - int c = Integer.parseInt(args[0]); - - for (int i = 1; i < args.length; i++) { - String candidate = args[i]; - candidates.add(candidate); - } - - return c; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); - } -} diff --git a/src/main/java/scalive/server/ILoopWithCompletion.java b/src/main/java/scalive/server/ILoopWithCompletion.java index 45f444c..49bf015 100644 --- a/src/main/java/scalive/server/ILoopWithCompletion.java +++ b/src/main/java/scalive/server/ILoopWithCompletion.java @@ -3,20 +3,20 @@ import scala.tools.nsc.interpreter.Completion; import scala.tools.nsc.interpreter.ILoop; import scala.tools.nsc.interpreter.IMain; -import scala.tools.nsc.interpreter.JLineCompletion; +import scala.tools.nsc.interpreter.PresentationCompilerCompleter; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; class ILoopWithCompletion extends ILoop { private Completion completion = null; - ILoopWithCompletion(InputStream in, OutputStream out) throws UnsupportedEncodingException { - super(new BufferedReader(new InputStreamReader(in, "UTF-8")), new PrintWriter(out)); + ILoopWithCompletion(InputStream in, OutputStream out) { + super(new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)), new PrintWriter(out)); } Completion getCompletion() { @@ -27,6 +27,6 @@ Completion getCompletion() { public void createInterpreter() { super.createInterpreter(); IMain intp = intp(); - completion = new JLineCompletion(intp); + completion = new PresentationCompilerCompleter(intp); } } diff --git a/src/main/java/scalive/server/Server.java b/src/main/java/scalive/server/Server.java index c3c8492..e9fc800 100644 --- a/src/main/java/scalive/server/Server.java +++ b/src/main/java/scalive/server/Server.java @@ -36,8 +36,8 @@ static void run( Runnable socketCleaner = Net.getSocketCleaner(replSocket, completerSocket); - ILoopWithCompletion iloop = Repl.run(replSocket, cl, socketCleaner); - Completer.run(completerSocket, iloop, socketCleaner); + ILoopWithCompletion iloop = ServerRepl.run(replSocket, cl, socketCleaner); + ServerCompleter.run(completerSocket, iloop, socketCleaner); } private static void loadDependencyJars( diff --git a/src/main/java/scalive/server/Completer.java b/src/main/java/scalive/server/ServerCompleter.java similarity index 83% rename from src/main/java/scalive/server/Completer.java rename to src/main/java/scalive/server/ServerCompleter.java index fe7dbdc..5dfcc2d 100644 --- a/src/main/java/scalive/server/Completer.java +++ b/src/main/java/scalive/server/ServerCompleter.java @@ -14,18 +14,19 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; /** - * @see scalive.client.Completer + * @see scalive.client.ClientCompleter */ -class Completer { +class ServerCompleter { static void run( Socket socket, ILoopWithCompletion iloop, Runnable socketCleaner ) throws IOException, InterruptedException { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); Net.throwSocketTimeoutExceptionForLongInactivity(socket); try { @@ -42,16 +43,16 @@ static void run( String buffer = line.substring(idx + 1); Completion completion = getCompletion(iloop); - Candidates candidates = completion.completer().complete(buffer, cursor); + Candidates candidates = completion.complete(buffer, cursor); - out.write(("" + candidates.cursor()).getBytes("UTF-8")); + out.write(("" + candidates.cursor()).getBytes(StandardCharsets.UTF_8)); List list = candidates.candidates(); Iterator it = list.iterator(); while (it.hasNext()) { String candidate = it.next(); out.write(' '); - out.write(candidate.getBytes("UTF-8")); + out.write(candidate.getBytes(StandardCharsets.UTF_8)); } out.write('\n'); @@ -68,7 +69,7 @@ static void run( Log.log("Completer closed"); } - static private Completion getCompletion(ILoopWithCompletion iloop) { + static private Completion getCompletion(ILoopWithCompletion iloop) throws InterruptedException { while (true) { // iloop may recreate completion as crash recovery, so do not cache it locally Completion completion = iloop.getCompletion(); @@ -76,11 +77,7 @@ static private Completion getCompletion(ILoopWithCompletion iloop) { if (completion != null) return completion; // Wait for completion to be created by iloop - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - Thread.interrupted(); - } + Thread.sleep(1000); } } } diff --git a/src/main/java/scalive/server/Repl.java b/src/main/java/scalive/server/ServerRepl.java similarity index 85% rename from src/main/java/scalive/server/Repl.java rename to src/main/java/scalive/server/ServerRepl.java index c7bd3ac..9f2d25d 100644 --- a/src/main/java/scalive/server/Repl.java +++ b/src/main/java/scalive/server/ServerRepl.java @@ -16,7 +16,7 @@ import java.net.Socket; import java.net.URLClassLoader; -class Repl { +class ServerRepl { /** Creates a REPL and wire IO streams of the socket to it. */ static ILoopWithCompletion run( final Socket socket, URLClassLoader cl, final Runnable socketCleaner @@ -28,20 +28,17 @@ static ILoopWithCompletion run( final Settings settings = getSettings(cl); Net.throwSocketTimeoutExceptionForLongInactivity(socket); - new Thread(Repl.class.getName() + "-iloop") { + new Thread(ServerRepl.class.getName() + "-iloop") { @Override public void run() { - overrideScalaConsole(in, out, new Runnable() { - @Override - public void run() { - // This call does not return until socket is closed, - // or repl has been closed by the client using ":q" - try { - iloop.process(settings); - } catch (Exception e) { - // See throwSocketTimeoutExceptionForLongInactivity above; - // just let this thread ends - } + overrideScalaConsole(in, out, () -> { + // This call does not return until socket is closed, + // or repl has been closed by the client using ":q" + try { + iloop.process(settings); + } catch (Exception e) { + // See throwSocketTimeoutExceptionForLongInactivity above; + // just let this thread ends } }); @@ -77,7 +74,7 @@ private static Settings getSettings(URLClassLoader cl) { // Without this class loader setting, the REPL and the target process will // see different instances of a static variable of the same class! // http://stackoverflow.com/questions/5950025/multiple-instances-of-static-variables - settings.explicitParentLoader_$eq(Option.apply((ClassLoader) cl)); + settings.explicitParentLoader_$eq(Option.apply(cl)); return settings; }