├── dev ├── zip │ ├── .gitignore │ ├── scalive.bat │ └── scalive └── README.rst ├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── src └── main │ └── java │ └── scalive │ ├── Log.java │ ├── server │ ├── ILoopWithCompletion.java │ ├── Agent.java │ ├── Server.java │ ├── ServerCompleter.java │ └── ServerRepl.java │ ├── client │ ├── Client.java │ ├── ClientCompleter.java │ ├── ClientRepl.java │ └── AgentLoader.java │ ├── Net.java │ └── Classpath.java ├── MIT-LICENSE └── README.md /dev/zip/.gitignore: -------------------------------------------------------------------------------- 1 | *.jar 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | project/project 3 | project/target 4 | target 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Run "sbt eclipse" to create Eclipse project file 2 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") 3 | -------------------------------------------------------------------------------- /dev/zip/scalive.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set JAVA_OPTS=-Djava.awt.headless=true 4 | 5 | set ROOT_DIR=%~dp0 6 | cd "%ROOT_DIR%" 7 | 8 | set CLASS_PATH="%ROOT_DIR%\*;." 9 | 10 | java %JAVA_OPTS% -cp %CLASS_PATH% scalive.client.AgentLoader %ROOT_DIR% %* 11 | -------------------------------------------------------------------------------- /src/main/java/scalive/Log.java: -------------------------------------------------------------------------------- 1 | package scalive; 2 | 3 | public class Log { 4 | public static void log(String msg) { 5 | System.out.println("[Scalive] " + msg); 6 | } 7 | 8 | public static void logNoTag(String msg) { 9 | System.out.println(msg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dev/zip/scalive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | JAVA_OPTS='-Djava.awt.headless=true' 4 | 5 | # Quote because path may contain spaces 6 | if [ -h $0 ] 7 | then 8 | ROOT_DIR="$(cd "$(dirname "$(readlink -n "$0")")" && pwd)" 9 | else 10 | ROOT_DIR="$(cd "$(dirname $0)" && pwd)" 11 | fi 12 | cd "$ROOT_DIR" 13 | 14 | CLASS_PATH="$ROOT_DIR/*:." 15 | 16 | java $JAVA_OPTS -cp $CLASS_PATH scalive.client.AgentLoader $ROOT_DIR $@ 17 | -------------------------------------------------------------------------------- /src/main/java/scalive/server/ILoopWithCompletion.java: -------------------------------------------------------------------------------- 1 | package scalive.server; 2 | 3 | import scala.tools.nsc.interpreter.Completion; 4 | import scala.tools.nsc.interpreter.ILoop; 5 | import scala.tools.nsc.interpreter.IMain; 6 | import scala.tools.nsc.interpreter.PresentationCompilerCompleter; 7 | 8 | import java.io.BufferedReader; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.OutputStream; 12 | import java.io.PrintWriter; 13 | import java.nio.charset.StandardCharsets; 14 | 15 | class ILoopWithCompletion extends ILoop { 16 | private Completion completion = null; 17 | 18 | ILoopWithCompletion(InputStream in, OutputStream out) { 19 | super(new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)), new PrintWriter(out)); 20 | } 21 | 22 | Completion getCompletion() { 23 | return completion; 24 | } 25 | 26 | @Override 27 | public void createInterpreter() { 28 | super.createInterpreter(); 29 | IMain intp = intp(); 30 | completion = new PresentationCompilerCompleter(intp); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ngoc Dao 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/main/java/scalive/client/Client.java: -------------------------------------------------------------------------------- 1 | package scalive.client; 2 | 3 | import scala.tools.jline_embedded.console.ConsoleReader; 4 | 5 | import scalive.Log; 6 | import scalive.Net; 7 | 8 | import java.net.Socket; 9 | 10 | class Client { 11 | static void run(int port) throws Exception { 12 | Log.log("Attach to target process at port " + port); 13 | final Socket replSocket = new Socket(Net.LOCALHOST, port); 14 | final Socket completerSocket = new Socket(Net.LOCALHOST, port); 15 | 16 | // Try to notify the target process to clean up when the client is terminated 17 | final Runnable socketCleaner = Net.getSocketCleaner(replSocket, completerSocket); 18 | Runtime.getRuntime().addShutdownHook(new Thread(Client.class.getName() + "-ShutdownHook") { 19 | @Override 20 | public void run() { 21 | socketCleaner.run(); 22 | } 23 | }); 24 | 25 | ConsoleReader reader = new ConsoleReader(System.in, System.out); 26 | ClientCompleter.setup(completerSocket, reader); 27 | ClientRepl.run(replSocket, reader); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dev/README.rst: -------------------------------------------------------------------------------- 1 | Control flow 2 | ------------ 3 | 4 | :: 5 | 6 | AgentLoader ----- attaches Agent ---------------> Target process 7 | passes: * Agent loads Server 8 | * TCP port * Server listens on the 9 | * jarpaths specified TCP port 10 | 11 | :: 12 | 13 | AgentLoader ----- Client connects to the port --> Target process 14 | * Server loads Repl 15 | ----- Keyboard input --> 16 | <---- Repl output --- 17 | 18 | zip directory 19 | ------------- 20 | 21 | This is the directory that will be zipped when Scalive is released. 22 | 23 | :: 24 | 25 | zip/ 26 | scalive 27 | scalive.bat 28 | scalive-.jar <- ../../target/scala-2.11/scalive-.jar 29 | 30 | scala-library-2.12.8.jar 31 | scala-compiler-2.12.8.jar 32 | scala-reflect-2.12.8.jar 33 | 34 | While developing: 35 | 36 | * Run ``sbt package`` to create/update scalive-.jar. 37 | * Add missing JARs as above. 38 | * Run ``scalive`` to attach to a JVM process to see if it works properly. 39 | 40 | Release 41 | ------- 42 | 43 | Based on the ``zip`` directory above, prepare a directory to be zipped and 44 | released (remember to remove unneccessary files, like .gitignore): 45 | 46 | :: 47 | 48 | scalive-/ 49 | scalive 50 | scalive.bat 51 | scalive-.jar <- Doesn't depend on Scala, thus doesn't follow Scala JAR naming 52 | 53 | scala-library-2.12.8.jar 54 | scala-compiler-2.12.8.jar 55 | scala-reflect-2.12.8.jar 56 | 57 | README.md 58 | 59 | Then zip it: 60 | 61 | :: 62 | 63 | zip -r scalive-.zip scalive- 64 | -------------------------------------------------------------------------------- /src/main/java/scalive/client/ClientCompleter.java: -------------------------------------------------------------------------------- 1 | package scalive.client; 2 | 3 | import scala.tools.jline_embedded.console.ConsoleReader; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStream; 9 | import java.net.Socket; 10 | import java.nio.charset.StandardCharsets; 11 | import java.util.List; 12 | 13 | /** 14 | * Input: cursor buffer 15 | * 16 | * Output: cursor candidate1 candidate2 candidate3... 17 | */ 18 | class ClientCompleter { 19 | static void setup(Socket socket, ConsoleReader reader) throws Exception { 20 | final InputStream in = socket.getInputStream(); 21 | final OutputStream out = socket.getOutputStream(); 22 | 23 | final BufferedReader b = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); 24 | 25 | reader.addCompleter((String buffer, int cursor, List candidates) -> { 26 | try { 27 | String request = String.format("%d %s\n", cursor, buffer); 28 | out.write(request.getBytes(StandardCharsets.UTF_8)); 29 | out.flush(); 30 | 31 | String response = b.readLine(); 32 | 33 | // socket closed; the client should exit 34 | if (response == null) return -1; 35 | 36 | String[] args = response.split(" "); 37 | 38 | int c = Integer.parseInt(args[0]); 39 | 40 | for (int i = 1; i < args.length; i++) { 41 | String candidate = args[i]; 42 | candidates.add(candidate); 43 | } 44 | 45 | return c; 46 | } catch (Exception e) { 47 | throw new RuntimeException(e); 48 | } 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/scalive/server/Agent.java: -------------------------------------------------------------------------------- 1 | package scalive.server; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.lang.instrument.Instrumentation; 6 | import java.net.ServerSocket; 7 | 8 | public class Agent { 9 | /** 10 | * @param agentArgs [...] 11 | * 12 | *
{@code
13 |      * jarSearchDir/
14 |      *   scalive.jar
15 |      *
16 |      *   scala-library-2.11.0.jar
17 |      *   scala-compiler-2.11.0.jar
18 |      *   scala-reflect-2.11.0.jar
19 |      *
20 |      *   [Other Scala versions]
21 |      * }
22 | */ 23 | public static void agentmain(String agentArgs, Instrumentation inst) throws IOException { 24 | final String[] args = agentArgs.split(" "); 25 | final String[] jarSearchDirs = args[0].split(File.pathSeparator); 26 | final int port = Integer.parseInt(args[1]); 27 | 28 | // Need to open server socket first before creating the thread below and returning, 29 | // otherwise the client won't be able to connect 30 | final ServerSocket serverSocket = Server.open(port); 31 | 32 | // Need to start a new thread because: 33 | // - The server is blocking for connections 34 | // - VirtualMachine#loadAgent at the client does not return until this agentmain method returns 35 | // - The client only connects to the server after VirtualMachine#loadAgent returns 36 | new Thread(Agent.class.getName() + "-Server") { 37 | @Override 38 | public void run() { 39 | try { 40 | Server.run(serverSocket, jarSearchDirs); 41 | } catch (Exception e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | }.start(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/scalive/Net.java: -------------------------------------------------------------------------------- 1 | package scalive; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | import java.net.ServerSocket; 6 | import java.net.Socket; 7 | import java.net.SocketException; 8 | import java.net.SocketTimeoutException; 9 | import java.net.UnknownHostException; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | public class Net { 13 | // After this time, the REPL and completer connections should be closed, 14 | // to avoid blocking socket reads to infinitely block threads created by Scalive in target process 15 | private static final int LONG_INACTIVITY = (int) TimeUnit.HOURS.toMillis(1); 16 | 17 | public static final InetAddress LOCALHOST = getLocalHostAddress(); 18 | 19 | public static int getLocalFreePort() throws Exception { 20 | ServerSocket server = new ServerSocket(0, 0, Net.LOCALHOST); 21 | int port = server.getLocalPort(); 22 | server.close(); 23 | return port; 24 | } 25 | 26 | /** 27 | * {@link SocketTimeoutException} will be thrown if there's no activity for a long time. 28 | * This is to avoid blocking reads to block threads infinitely, causing leaks in the target process. 29 | */ 30 | public static void throwSocketTimeoutExceptionForLongInactivity(Socket socket) throws SocketException { 31 | socket.setSoTimeout(LONG_INACTIVITY); 32 | } 33 | 34 | /** 35 | * Use socket closing as a way to notify/cleanup socket blocking read. 36 | * The sockets are closed in the order they are given. 37 | */ 38 | public static Runnable getSocketCleaner(final Socket... sockets) { 39 | return () -> { 40 | for (Socket socket : sockets) { 41 | try { 42 | socket.close(); 43 | } catch (IOException e) { 44 | // Ignore 45 | } 46 | } 47 | }; 48 | } 49 | 50 | private static InetAddress getLocalHostAddress() { 51 | try { 52 | return InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); 53 | } catch (UnknownHostException e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/scalive/client/ClientRepl.java: -------------------------------------------------------------------------------- 1 | package scalive.client; 2 | 3 | import scala.tools.jline_embedded.console.ConsoleReader; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.io.OutputStream; 9 | import java.net.Socket; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | class ClientRepl { 13 | static void run(Socket socket, final ConsoleReader reader) throws IOException { 14 | final InputStream in = socket.getInputStream(); 15 | final OutputStream out = socket.getOutputStream(); 16 | 17 | new Thread(ClientRepl.class.getName() + "-printServerOutput") { 18 | @Override 19 | public void run() { 20 | try { 21 | printServerOutput(in); 22 | } catch (Exception e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | }.start(); 27 | 28 | readLocalInput(reader, out); 29 | } 30 | 31 | private static void readLocalInput(ConsoleReader reader, OutputStream out) throws IOException { 32 | while (true) { 33 | // Read 34 | String line = reader.readLine(); 35 | if (line == null) break; 36 | 37 | // Evaluate 38 | try { 39 | out.write(line.getBytes(StandardCharsets.UTF_8)); 40 | out.write('\n'); 41 | out.flush(); 42 | } catch (IOException e) { 43 | // Socket closed 44 | break; 45 | } 46 | } 47 | } 48 | 49 | private static void printServerOutput(InputStream in) { 50 | InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8); 51 | while (true) { 52 | int i; 53 | try { 54 | i = reader.read(); 55 | } catch (IOException e) { 56 | // Socket closed 57 | break; 58 | } 59 | if (i < 0) break; 60 | 61 | System.out.print((char) i); 62 | System.out.flush(); 63 | } 64 | 65 | // The loop above is broken when REPL is closed by the target process; exit now 66 | System.exit(0); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/scalive/server/Server.java: -------------------------------------------------------------------------------- 1 | package scalive.server; 2 | 3 | import scalive.Classpath; 4 | import scalive.Log; 5 | import scalive.Net; 6 | 7 | import java.io.IOException; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.net.MalformedURLException; 10 | import java.net.ServerSocket; 11 | import java.net.Socket; 12 | import java.net.URLClassLoader; 13 | 14 | class Server { 15 | static ServerSocket open(int port) throws IOException { 16 | Log.log("Open local port " + port + " for REPL and completer"); 17 | return new ServerSocket(port, 1, Net.LOCALHOST); 18 | } 19 | 20 | static void run( 21 | ServerSocket serverSocket, String[] jarSearchDirs 22 | ) throws IOException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException, IllegalAccessException, InterruptedException { 23 | // Accept 2 connections (blocking) 24 | Socket replSocket = serverSocket.accept(); 25 | Log.log("REPL connected"); 26 | 27 | Socket completerSocket = serverSocket.accept(); 28 | Log.log("Completer connected"); 29 | 30 | // Accept no further connections 31 | serverSocket.close(); 32 | 33 | // Load dependency JARs 34 | URLClassLoader cl = (URLClassLoader) ClassLoader.getSystemClassLoader(); 35 | loadDependencyJars(cl, jarSearchDirs); 36 | 37 | Runnable socketCleaner = Net.getSocketCleaner(replSocket, completerSocket); 38 | 39 | ILoopWithCompletion iloop = ServerRepl.run(replSocket, cl, socketCleaner); 40 | ServerCompleter.run(completerSocket, iloop, socketCleaner); 41 | } 42 | 43 | private static void loadDependencyJars( 44 | URLClassLoader cl, String[] jarSearchDirs 45 | ) throws IllegalAccessException, InvocationTargetException, MalformedURLException, NoSuchMethodException, ClassNotFoundException { 46 | // Try scala-library first 47 | Classpath.findAndAddJar(cl, "scala.AnyVal", jarSearchDirs, "scala-library"); 48 | 49 | // So that we can get the actual Scala version being used 50 | String version = Classpath.getScalaVersion(cl); 51 | 52 | Classpath.findAndAddJar(cl, "scala.reflect.runtime.JavaUniverse", jarSearchDirs, "scala-reflect-" + version); 53 | Classpath.findAndAddJar(cl, "scala.tools.nsc.interpreter.ILoop", jarSearchDirs, "scala-compiler-" + version); 54 | 55 | Classpath.findAndAddJar(cl, "scalive.Repl", jarSearchDirs, "scalive"); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/scalive/server/ServerCompleter.java: -------------------------------------------------------------------------------- 1 | package scalive.server; 2 | 3 | import scala.collection.Iterator; 4 | import scala.collection.immutable.List; 5 | import scala.tools.nsc.interpreter.Completion; 6 | import scala.tools.nsc.interpreter.Completion.Candidates; 7 | 8 | import scalive.Log; 9 | import scalive.Net; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.io.OutputStream; 16 | import java.net.Socket; 17 | import java.nio.charset.StandardCharsets; 18 | 19 | /** 20 | * @see scalive.client.ClientCompleter 21 | */ 22 | class ServerCompleter { 23 | static void run( 24 | Socket socket, ILoopWithCompletion iloop, Runnable socketCleaner 25 | ) throws IOException, InterruptedException { 26 | InputStream in = socket.getInputStream(); 27 | OutputStream out = socket.getOutputStream(); 28 | 29 | BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); 30 | 31 | Net.throwSocketTimeoutExceptionForLongInactivity(socket); 32 | try { 33 | while (true) { 34 | // See throwSocketTimeoutExceptionForLongInactivity above 35 | String line = reader.readLine(); 36 | 37 | // Socket closed 38 | if (line == null) break; 39 | 40 | int idx = line.indexOf(" "); 41 | String cursorString = line.substring(0, idx); 42 | int cursor = Integer.parseInt(cursorString); 43 | String buffer = line.substring(idx + 1); 44 | 45 | Completion completion = getCompletion(iloop); 46 | Candidates candidates = completion.complete(buffer, cursor); 47 | 48 | out.write(("" + candidates.cursor()).getBytes(StandardCharsets.UTF_8)); 49 | 50 | List list = candidates.candidates(); 51 | Iterator it = list.iterator(); 52 | while (it.hasNext()) { 53 | String candidate = it.next(); 54 | out.write(' '); 55 | out.write(candidate.getBytes(StandardCharsets.UTF_8)); 56 | } 57 | 58 | out.write('\n'); 59 | out.flush(); 60 | } 61 | } catch (IOException e) { 62 | // Socket closed 63 | } 64 | 65 | socketCleaner.run(); 66 | 67 | // Before logging this out, wait a litte for System.out to be restored back to the target process 68 | Thread.sleep(1000); 69 | Log.log("Completer closed"); 70 | } 71 | 72 | static private Completion getCompletion(ILoopWithCompletion iloop) throws InterruptedException { 73 | while (true) { 74 | // iloop may recreate completion as crash recovery, so do not cache it locally 75 | Completion completion = iloop.getCompletion(); 76 | 77 | if (completion != null) return completion; 78 | 79 | // Wait for completion to be created by iloop 80 | Thread.sleep(1000); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/scalive/client/AgentLoader.java: -------------------------------------------------------------------------------- 1 | package scalive.client; 2 | 3 | // http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/index.html 4 | import com.sun.tools.attach.VirtualMachine; 5 | import com.sun.tools.attach.VirtualMachineDescriptor; 6 | 7 | import scalive.Classpath; 8 | import scalive.Log; 9 | import scalive.Net; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.net.URLClassLoader; 14 | 15 | public class AgentLoader { 16 | /** 17 | * @param args [...] [pid] 18 | * 19 | *
{@code
 20 |      * jarSearchDir/
 21 |      *   scalive.jar
 22 |      *   jline-2.14.2.jar
 23 |      *
 24 |      *   scala-library-2.11.0.jar
 25 |      *   scala-compiler-2.11.0.jar
 26 |      *   scala-reflect-2.11.0.jar
 27 |      *
 28 |      *   [Other Scala versions]
 29 |      * }
30 | */ 31 | public static void main(String[] args) throws Exception { 32 | if (args.length != 1 && args.length != 2) { 33 | Log.log("Arguments: [...] [pid]"); 34 | return; 35 | } 36 | 37 | addToolsDotJarToClasspath(); 38 | 39 | if (args.length == 1) { 40 | listJvmProcesses(); 41 | return; 42 | } 43 | 44 | String jarSearchDirs = args[0]; 45 | String pid = args[1]; 46 | int port = loadAgent(jarSearchDirs, pid); 47 | Client.run(port); 48 | } 49 | 50 | /** 51 | * com.sun.tools.attach.VirtualMachine is in tools.jar, which is not in 52 | * classpath by default: 53 | * 54 | *
{@code
 55 |      * jdk/
 56 |      *   bin/
 57 |      *     java
 58 |      *     javac
 59 |      *   jre/
 60 |      *     java
 61 |      *   lib/
 62 |      *     tools.jar
 63 |      * }
64 | */ 65 | private static void addToolsDotJarToClasspath() throws Exception { 66 | String path = System.getProperty("java.home") + "/../lib/tools.jar"; 67 | Classpath.addPath((URLClassLoader) ClassLoader.getSystemClassLoader(), path); 68 | } 69 | 70 | private static void listJvmProcesses() { 71 | Log.logNoTag("JVM processes:"); 72 | Log.logNoTag("#pid\tDisplay name"); 73 | 74 | for (VirtualMachineDescriptor vmd : VirtualMachine.list()) { 75 | Log.logNoTag(vmd.id() + "\t" + vmd.displayName()); 76 | } 77 | } 78 | 79 | private static int loadAgent(String jarSearchDirs, String pid) throws Exception { 80 | final String[] ary = jarSearchDirs.split(File.pathSeparator); 81 | final String agentJar = Classpath.findJar(ary, "scalive"); 82 | final VirtualMachine vm = VirtualMachine.attach(pid); 83 | final int port = Net.getLocalFreePort(); 84 | 85 | vm.loadAgent(agentJar, jarSearchDirs + " " + port); 86 | Runtime.getRuntime().addShutdownHook(new Thread(AgentLoader.class.getName() + "-ShutdownHook") { 87 | @Override 88 | public void run() { 89 | try { 90 | vm.detach(); 91 | } catch (IOException e) { 92 | throw new RuntimeException(e); 93 | } 94 | } 95 | }); 96 | 97 | return port; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scalive 2 | 3 | This tool allows you to connect a Scala REPL console to running Oracle (Sun) 4 | JVM processes without any prior setup at the target process. 5 | 6 | [![View demo video on YouTube](http://img.youtube.com/vi/h45QQ45D9P8/0.jpg)](http://www.youtube.com/watch?v=h45QQ45D9P8) 7 | 8 | ## Download 9 | 10 | For Scala 2.12, download 11 | [scalive-1.7.0.zip](https://github.com/xitrum-framework/scalive/releases/download/1.7.0/scalive-1.7.0.zip). 12 | 13 | For Scala 2.10 and 2.11, download 14 | [scalive-1.6.zip](https://github.com/xitrum-framework/scalive/releases/download/1.6/scalive-1.6.zip). 15 | 16 | Extract the ZIP file, you will see: 17 | 18 | ```txt 19 | scalive-1.7.0/ 20 | scalive 21 | scalive.bat 22 | scalive-1.7.0.jar 23 | 24 | scala-library-2.12.8.jar 25 | scala-compiler-2.12.8.jar 26 | scala-reflect-2.12.8.jar 27 | ``` 28 | 29 | scala-library, scala-compiler, and scala-reflect of the correct version 30 | that your JVM process is using will be loaded, if they have not been loaded yet. 31 | The REPL console needs these libraries to work. 32 | 33 | For example, your process has already loaded scala-library 2.12.8 by itself, 34 | but scala-compiler and scala-reflect haven't been loaded, Scalive will 35 | automatically load their version 2.12.8. 36 | 37 | If none of them has been loaded, i.e. your process doesn't use Scala, 38 | Scalive will load the lastest version in the directory. 39 | 40 | For your convenience, Scala 2.12.8 JAR files have been included above. 41 | 42 | If your process uses a different Scala version, you need to manually 43 | download the corresponding JARs from the Internet and save them in the 44 | same directory as above. 45 | 46 | ## Usage 47 | 48 | Run the shell script `scalive` (*nix) or `scalive.bat` (Windows). 49 | 50 | Run without argument to see the list of running JVM process IDs on your local machine: 51 | 52 | ```sh 53 | scalive 54 | ``` 55 | 56 | Example output: 57 | 58 | ```txt 59 | JVM processes: 60 | #pid Display name 61 | 13821 demos.Boot 62 | 17978 quickstart.Boot 63 | ``` 64 | 65 | To connect a Scala REPL console to a process: 66 | 67 | ```sh 68 | scalive 69 | ``` 70 | 71 | Just like in normal Scala REPL console, you can: 72 | 73 | * Use up/down arrows keys to navigate the console history 74 | * Use tab key for completion 75 | 76 | ## How to load your own JARs to the process 77 | 78 | Scalive only automatically loads `scala-library.jar`, `scala-compiler.jar`, 79 | `scala-reflect.jar`, and `scalive.jar` to the system classpath. 80 | 81 | If you want to load additional classes in other JARs, first run these in the 82 | REPL console to load the JAR to the system class loader: 83 | 84 | ```scala 85 | val cl = ClassLoader.getSystemClassLoader.asInstanceOf[java.net.URLClassLoader] 86 | val jarSearchDirs = Array("/dir/containing/the/jar") 87 | val jarPrefix = "mylib" // Will match "mylib-xxx.jar", convenient when there's version number in the file name 88 | scalive.Classpath.findAndAddJar(cl, jarSearchDirs, jarPrefix) 89 | ``` 90 | 91 | Now the trick is just quit the REPL console and connect it to the target process 92 | again. You will be able to use your classes in the JAR normally: 93 | 94 | ```scala 95 | import mylib.foo.Bar 96 | ... 97 | ``` 98 | 99 | [Note that `:cp` doesn't work](http://stackoverflow.com/questions/18033752/cannot-add-a-jar-to-scala-repl-with-the-cp-command). 100 | 101 | ## How Scalive works 102 | 103 | Scalive uses the [Attach API](https://blogs.oracle.com/CoreJavaTechTips/entry/the_attach_api) 104 | to tell the target process to load an [agent](http://javahowto.blogspot.jp/2006/07/javaagent-option.html). 105 | 106 | Inside the target progress, the agent creates a REPL interpreter and a 107 | TCP server to let the Scalive process connect and interact with the 108 | interpreter. The Scalive process acts as a TCP client. There are 2 TCP 109 | connections, one for REPL data and one for tab key completion data. 110 | 111 | Similar projects: 112 | 113 | * [liverepl](https://github.com/djpowell/liverepl) 114 | * [scala-web-repl](https://github.com/woshilaiceshide/scala-web-repl) 115 | 116 | ## Known issues 117 | 118 | For simplicity and to avoid memory leak when you attach/detach many times, 119 | Scalive only supports processes with only the default system class loader, 120 | without additional class loaders. Usually they are standalone JVM processes, 121 | like 122 | [Play](http://www.playframework.com) or 123 | [Xitrum](http://xitrum-framework.github.io) in production mode. 124 | 125 | Processes with multiple class loaders like Tomcat are currently not supported. 126 | -------------------------------------------------------------------------------- /src/main/java/scalive/Classpath.java: -------------------------------------------------------------------------------- 1 | package scalive; 2 | 3 | import java.io.File; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | import java.net.MalformedURLException; 7 | import java.net.URL; 8 | import java.net.URLClassLoader; 9 | import java.util.Arrays; 10 | import java.util.Objects; 11 | import java.util.stream.Collectors; 12 | 13 | public class Classpath { 14 | private static final Method addURL = getAddURL(); 15 | 16 | // http://stackoverflow.com/questions/8222976/why-urlclassloader-addurl-protected-in-java 17 | private static Method getAddURL() { 18 | try { 19 | Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); 20 | method.setAccessible(true); 21 | return method; 22 | } catch (NoSuchMethodException e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | 27 | //-------------------------------------------------------------------------- 28 | 29 | /** 30 | * Finds the ".jar" file with the {@code jarPrefix} and the latest version. 31 | * 32 | * @param jarSearchDirs Directories to search for the JAR 33 | * 34 | * @param jarPrefix JAR file name prefix to search; example: "scalive-agent" will match "scalive-agent-xxx.jar" 35 | */ 36 | public static String findJar(String[] jarSearchDirs, String jarPrefix) throws IllegalStateException { 37 | String maxBaseName = null; 38 | File maxFile = null; 39 | for (String jarSearchDir: jarSearchDirs) { 40 | File dir = new File(jarSearchDir); 41 | File[] files = dir.listFiles(); 42 | if (files == null) continue; 43 | 44 | for (File file : files) { 45 | String baseName = file.getName(); 46 | if (file.isFile() && baseName.endsWith(".jar") && baseName.startsWith(jarPrefix)) { 47 | if (maxBaseName == null || baseName.compareTo(maxBaseName) > 0) { 48 | maxBaseName = baseName; 49 | maxFile = file; 50 | } 51 | } 52 | } 53 | } 54 | 55 | if (maxFile == null) 56 | throw new IllegalStateException("Could not find " + jarPrefix + " in " + String.join(File.pathSeparator, jarSearchDirs)); 57 | else 58 | return maxFile.getPath(); 59 | } 60 | 61 | public static void addPath( 62 | URLClassLoader cl, String path 63 | ) throws MalformedURLException, InvocationTargetException, IllegalAccessException { 64 | URL url = new File(path).toURI().toURL(); 65 | URL[] urls = cl.getURLs(); 66 | if (!Arrays.asList(urls).contains(url)) addURL.invoke(cl, url); 67 | } 68 | 69 | /** Combination of {@link #findJar(String[], String)} and {@link #addPath(URLClassLoader, String)}. */ 70 | public static void findAndAddJar( 71 | URLClassLoader cl, String[] jarSearchDirs, String jarPrefix 72 | ) throws IllegalAccessException, InvocationTargetException, MalformedURLException { 73 | String jar = findJar(jarSearchDirs, jarPrefix); 74 | addPath(cl, jar); 75 | Log.log("Load " + jar); 76 | } 77 | 78 | /** 79 | * Similar to {@link #findAndAddJar(URLClassLoader, String[], String)} without {@code representativeClass}, 80 | * but only find and add the JAR to classpath if the representativeClass has not been loaded. 81 | */ 82 | public static void findAndAddJar( 83 | URLClassLoader cl, String representativeClass, String[] jarSearchDirs, String jarPrefix 84 | ) throws IllegalAccessException, MalformedURLException, InvocationTargetException { 85 | try { 86 | Class.forName(representativeClass, true, cl); 87 | } catch (ClassNotFoundException e) { 88 | findAndAddJar(cl, jarSearchDirs, jarPrefix); 89 | } 90 | } 91 | 92 | // http://stackoverflow.com/questions/4121567/embedded-scala-repl-inherits-parent-classpath 93 | public static String getClasspath(URLClassLoader cl) { 94 | URL[] urls = cl.getURLs(); 95 | return Arrays.stream(urls).map(Objects::toString).collect(Collectors.joining(File.pathSeparator)); 96 | } 97 | 98 | public static String getScalaVersion( 99 | ClassLoader cl 100 | ) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 101 | Class k = Class.forName("scala.util.Properties", true, cl); 102 | Method m = k.getDeclaredMethod("versionNumberString"); 103 | return (String) m.invoke(k); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/scalive/server/ServerRepl.java: -------------------------------------------------------------------------------- 1 | package scalive.server; 2 | 3 | import scala.Console; 4 | import scala.Option; 5 | import scala.runtime.AbstractFunction0; 6 | import scala.tools.nsc.Settings; 7 | 8 | import scalive.Classpath; 9 | import scalive.Log; 10 | import scalive.Net; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | import java.io.PrintStream; 16 | import java.net.Socket; 17 | import java.net.URLClassLoader; 18 | 19 | class ServerRepl { 20 | /** Creates a REPL and wire IO streams of the socket to it. */ 21 | static ILoopWithCompletion run( 22 | final Socket socket, URLClassLoader cl, final Runnable socketCleaner 23 | ) throws IOException { 24 | final InputStream in = socket.getInputStream(); 25 | final OutputStream out = socket.getOutputStream(); 26 | 27 | final ILoopWithCompletion iloop = new ILoopWithCompletion(in, out); 28 | final Settings settings = getSettings(cl); 29 | 30 | Net.throwSocketTimeoutExceptionForLongInactivity(socket); 31 | new Thread(ServerRepl.class.getName() + "-iloop") { 32 | @Override 33 | public void run() { 34 | overrideScalaConsole(in, out, () -> { 35 | // This call does not return until socket is closed, 36 | // or repl has been closed by the client using ":q" 37 | try { 38 | iloop.process(settings); 39 | } catch (Exception e) { 40 | // See throwSocketTimeoutExceptionForLongInactivity above; 41 | // just let this thread ends 42 | } 43 | }); 44 | 45 | // This code should be put outside overrideScalaConsole above 46 | // so that the output is not redirected to the client, 47 | // in case repl has been closed by the client using ":q" 48 | socketCleaner.run(); 49 | Log.log("REPL closed"); 50 | } 51 | }.start(); 52 | 53 | return iloop; 54 | } 55 | 56 | private static Settings getSettings(URLClassLoader cl) { 57 | // Without the below settings, there will be error: 58 | // Failed to initialize compiler: object scala.runtime in compiler mirror not found. 59 | // ** Note that as of 2.8 scala does not assume use of the java classpath. 60 | // ** For the old behavior pass -usejavacp to scala, or if using a Settings 61 | // ** object programatically, settings.usejavacp.value = true. 62 | // 63 | // "usejavacp" (System.getProperty("java.class.path")) is not enough when 64 | // there are multiple class loaders (and even when there's only one class 65 | // loader but the "java.class.path" system property does not contain Scala JARs 66 | // 67 | // http://stackoverflow.com/questions/18150961/scala-runtime-in-compiler-mirror-not-found-but-working-when-started-with-xboo 68 | Settings settings = new Settings(); 69 | 70 | // http://www.scala-lang.org/old/node/8002 71 | String classpath = Classpath.getClasspath(cl); 72 | settings.classpath().value_$eq(classpath); 73 | 74 | // Without this class loader setting, the REPL and the target process will 75 | // see different instances of a static variable of the same class! 76 | // http://stackoverflow.com/questions/5950025/multiple-instances-of-static-variables 77 | settings.explicitParentLoader_$eq(Option.apply(cl)); 78 | 79 | return settings; 80 | } 81 | 82 | // https://github.com/xitrum-framework/scalive/issues/11 83 | // http://stackoverflow.com/questions/25623779/implementing-a-scala-function-in-java 84 | private static void overrideScalaConsole(final InputStream in, final OutputStream out, final Runnable runnable) { 85 | Console.withIn(in, new AbstractFunction0() { 86 | @Override 87 | public Object apply() { 88 | Console.withOut(out, new AbstractFunction0() { 89 | @Override 90 | public Object apply() { 91 | Console.withErr(out, new AbstractFunction0() { 92 | @Override 93 | public Object apply() { 94 | withIO(in, out, runnable); 95 | return null; 96 | } 97 | }); 98 | return null; 99 | } 100 | }); 101 | return null; 102 | } 103 | }); 104 | } 105 | 106 | private static void withIO(InputStream in, OutputStream out, Runnable runnable) { 107 | InputStream originalIn = System.in; 108 | PrintStream originalOut = System.out; 109 | PrintStream originalErr = System.err; 110 | 111 | try { 112 | System.setIn(in); 113 | System.setOut(new PrintStream(out)); 114 | System.setErr(new PrintStream(out)); 115 | 116 | runnable.run(); 117 | } finally { 118 | System.setIn(originalIn); 119 | System.setOut(originalOut); 120 | System.setErr(originalErr); 121 | } 122 | } 123 | } 124 | --------------------------------------------------------------------------------