Skip to content

Commit

Permalink
Fix #19 Not working with Scala 2.12 processes
Browse files Browse the repository at this point in the history
  • Loading branch information
ngocdaothanh committed Jan 30, 2019
1 parent 4fcba1e commit 703c590
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 177 deletions.
59 changes: 30 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
# 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.

[![View demo video on YouTube](http://img.youtube.com/vi/h45QQ45D9P8/0.jpg)](http://www.youtube.com/watch?v=h45QQ45D9P8)

## 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
Expand All @@ -63,7 +64,7 @@ JVM processes:

To connect a Scala REPL console to a process:

```
```sh
scalive <process id listed above>
```

Expand All @@ -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
Expand All @@ -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
...
```
Expand All @@ -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:

Expand All @@ -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.
13 changes: 7 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
27 changes: 10 additions & 17 deletions dev/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,12 @@ This is the directory that will be zipped when Scalive is released.

zip/
scalive
scalive.cmd
scalive.bat
scalive-<version>.jar <- ../../target/scala-2.11/scalive-<version>.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:

Expand All @@ -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-<version>/
scalive
scalive.cmd
scalive.bat
scalive-<version>.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:

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=0.13.12
sbt.version=1.2.8
4 changes: 2 additions & 2 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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")
19 changes: 5 additions & 14 deletions src/main/java/scalive/Classpath.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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();
}
}
15 changes: 6 additions & 9 deletions src/main/java/scalive/Net.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
};
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/scalive/client/Client.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package scalive.client;

import jline.console.ConsoleReader;
import scala.tools.jline_embedded.console.ConsoleReader;

import scalive.Log;
import scalive.Net;
Expand All @@ -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);
}
}
51 changes: 51 additions & 0 deletions src/main/java/scalive/client/ClientCompleter.java
Original file line number Diff line number Diff line change
@@ -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<CharSequence> 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);
}
});
}
}
Loading

0 comments on commit 703c590

Please sign in to comment.