Skip to content

Commit e628b63

Browse files
committed
Initial import.
0 parents  commit e628b63

File tree

9 files changed

+789
-0
lines changed

9 files changed

+789
-0
lines changed

Diff for: README.md

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#Attaching jshell#
2+
3+
JShell is a very useful new tool in JDK 9 that allows for interactive use of Java in a
4+
"read-eval-print loop". It is obvious that JShell would be particularly well suited for
5+
interactions with live JVMs, to examine their behavior in real time or to leverage their
6+
resources (e.g. if they have expensive objects already loaded, or if they run on powerful
7+
remote machines on which it is not practical to launch a JShell instance). Attaching to
8+
live JVMs would also provide an indirect way of populating objects in the JShell
9+
environment (a feature that
10+
[has been requested](http://mail.openjdk.java.net/pipermail/kulla-dev/2016-November/001774.html)).
11+
Unfortunately the current implementation of JShell does not give the option to connect to
12+
an already-running JVM (it starts a new JVM on the local host), although that
13+
[may change in the future](https://bugs.openjdk.java.net/browse/JDK-8131021).
14+
15+
This project provides a JShell execution engine that can attach to any already-running
16+
JVM, as long as that JVM has been started appropriately.
17+
18+
##Example usage##
19+
- Start the target JVM with
20+
`-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=XXXhostname:XXXport`
21+
(update `XXXhostname` and `XXXport` as appropriate) and call
22+
`new uk.org.cinquin.attaching_jshell.ExistingVMRemoteExecutionControl()` from that JVM
23+
prior to using JShell
24+
25+
- call JShell as follows:
26+
`java -cp lib/attaching_jshell.jar jdk.internal.jshell.tool.JShellToolProvider --execution "attachToExistingVM:hostname(XXXhostname),port(XXXport)"`
27+
using the same values of `XXXhostname` and `XXXport` as above
28+
29+
A simple way of making objects accessible to JShell is to have static fields point at
30+
them (see example in `ExistingVMRemoteExecutionControl` class).
31+
32+
The example commands above are provided in the test scripts `run_test_target` (to be
33+
executed first) and `run_jshell`. From the JShell instance, run for example
34+
```
35+
import uk.org.cinquin.attaching_jshell.ExistingVMRemoteExecutionControl;
36+
String s = ExistingVMRemoteExecutionControl.theGoodsForTesting
37+
```
38+
39+
##Implementation notes##
40+
The need to use the `ExistingVMRemoteExecutionControl` class from the target JVM stems
41+
from limitations in the Java Debug Interface (JDI). An alternative would be to use the
42+
JVM Tool Interface, which would require compiling platform-specific native binaries, or
43+
to use JDI in a more hackish way to get the JShell connection established.
44+
45+
##Note on security##
46+
Make sure that the debugging port created with the `-agentlib:jdwp` option shown above is
47+
not publicly exposed, as it can be exploited rather trivially for arbitrary code execution.
48+
49+
##Limitations##
50+
51+
- Only one JShell instance can be connected to a target VM at any given time. This limitation
52+
could probably be lifted with some work.
53+
- The standard error stream of the target JVM is captured by JShell and is currently not
54+
restored when JShell exits.
55+
- The versions of JShell on the target JVM and the one running JShell probably need to
56+
match. Note that this project has been tested with Oracle JDK early access build 161,
57+
which hopefully will be close to the final release of JDK 9.

Diff for: build.xml

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project name="jshell" default="all">
3+
4+
5+
<property file="jshell.properties"/>
6+
<!-- Uncomment the following property if no tests compilation is needed -->
7+
<!--
8+
<property name="skip.tests" value="true"/>
9+
-->
10+
11+
<!-- Compiler options -->
12+
13+
<property name="compiler.debug" value="on"/>
14+
<property name="compiler.generate.no.warnings" value="off"/>
15+
<property name="compiler.args" value=""/>
16+
<property name="compiler.max.memory" value="700m"/>
17+
<patternset id="ignored.files">
18+
<exclude name="**/CVS/**"/>
19+
<exclude name="**/SCCS/**"/>
20+
<exclude name="**/RCS/**"/>
21+
<exclude name="**/rcs/**"/>
22+
<exclude name="**/.DS_Store/**"/>
23+
<exclude name="**/.svn/**"/>
24+
<exclude name="**/.pyc/**"/>
25+
<exclude name="**/.pyo/**"/>
26+
<exclude name="**/*.pyc/**"/>
27+
<exclude name="**/*.pyo/**"/>
28+
<exclude name="**/.git/**"/>
29+
<exclude name="**/*.hprof/**"/>
30+
<exclude name="**/_svn/**"/>
31+
<exclude name="**/.hg/**"/>
32+
<exclude name="**/*~/**"/>
33+
<exclude name="**/__pycache__/**"/>
34+
<exclude name="**/*.rbc/**"/>
35+
<exclude name="**/vssver.scc/**"/>
36+
<exclude name="**/vssver2.scc/**"/>
37+
</patternset>
38+
<patternset id="library.patterns">
39+
<include name="*.egg"/>
40+
<include name="*.jar"/>
41+
<include name="*.ear"/>
42+
<include name="*.swc"/>
43+
<include name="*.war"/>
44+
<include name="*.ane"/>
45+
<include name="*.zip"/>
46+
</patternset>
47+
<patternset id="compiler.resources">
48+
<exclude name="**/?*.java"/>
49+
<exclude name="**/?*.form"/>
50+
<exclude name="**/?*.class"/>
51+
<exclude name="**/?*.groovy"/>
52+
<exclude name="**/?*.scala"/>
53+
<exclude name="**/?*.flex"/>
54+
<exclude name="**/?*.kt"/>
55+
<exclude name="**/?*.clj"/>
56+
<exclude name="**/?*.aj"/>
57+
</patternset>
58+
59+
<!-- Modules -->
60+
61+
62+
<!-- Module jshell -->
63+
64+
<dirname property="module.jshell.basedir" file="${ant.file}"/>
65+
66+
67+
68+
<property name="compiler.args.jshell" value="-encoding UTF-8 -source 9 -target 9 ${compiler.args}"/>
69+
70+
<property name="jshell.output.dir" value="build"/>
71+
<property name="jshell.testoutput.dir" value="test"/>
72+
73+
<path id="jshell.module.bootclasspath">
74+
<!-- Paths to be included in compilation bootclasspath -->
75+
</path>
76+
77+
<path id="jshell.module.production.classpath"/>
78+
79+
<path id="jshell.runtime.production.module.classpath">
80+
<pathelement location="${jshell.output.dir}"/>
81+
</path>
82+
83+
<path id="jshell.module.classpath">
84+
<pathelement location="${jshell.output.dir}"/>
85+
</path>
86+
87+
<path id="jshell.runtime.module.classpath">
88+
<pathelement location="${jshell.testoutput.dir}"/>
89+
<pathelement location="${jshell.output.dir}"/>
90+
</path>
91+
92+
93+
<patternset id="excluded.from.module.jshell">
94+
<patternset refid="ignored.files"/>
95+
</patternset>
96+
97+
<patternset id="excluded.from.compilation.jshell">
98+
<patternset refid="excluded.from.module.jshell"/>
99+
</patternset>
100+
101+
<path id="jshell.module.sourcepath">
102+
<dirset dir=".">
103+
<include name="src"/>
104+
<include name="src/main/java"/>
105+
</dirset>
106+
</path>
107+
108+
109+
<target name="compile.module.jshell" depends="compile.module.jshell.production,compile.module.jshell.tests" description="Compile module jshell"/>
110+
111+
<target name="compile.module.jshell.production" description="Compile module jshell; production classes">
112+
<mkdir dir="${jshell.output.dir}"/>
113+
<javac destdir="${jshell.output.dir}" debug="${compiler.debug}" nowarn="${compiler.generate.no.warnings}" memorymaximumsize="${compiler.max.memory}" fork="true">
114+
<compilerarg line="${compiler.args.jshell}"/>
115+
<bootclasspath refid="jshell.module.bootclasspath"/>
116+
<classpath refid="jshell.module.production.classpath"/>
117+
<src refid="jshell.module.sourcepath"/>
118+
<patternset refid="excluded.from.compilation.jshell"/>
119+
</javac>
120+
121+
</target>
122+
123+
<target name="compile.module.jshell.tests" depends="compile.module.jshell.production" description="compile module jshell; test classes" unless="skip.tests"/>
124+
125+
<target name="clean.module.jshell" description="cleanup module">
126+
<delete dir="${jshell.output.dir}"/>
127+
<delete dir="${jshell.testoutput.dir}"/>
128+
</target>
129+
130+
<target name="init" description="Build initialization">
131+
<!-- Perform any build initialization in this target -->
132+
</target>
133+
134+
<target name="clean" depends="clean.module.jshell" description="cleanup all"/>
135+
136+
<target name="build.modules" depends="init, clean, compile.module.jshell" description="build all modules"/>
137+
138+
<target name="init.artifacts">
139+
<property name="artifacts.temp.dir" value="${basedir}/__artifacts_temp"/>
140+
<property name="artifact.output.attaching_jshell" value="lib"/>
141+
<mkdir dir="${artifacts.temp.dir}"/>
142+
<property name="temp.jar.path.attaching_jshell.jar" value="${artifacts.temp.dir}/attaching_jshell.jar"/>
143+
</target>
144+
145+
<target name="artifact.attaching_jshell" depends="init.artifacts, compile.module.jshell" description="Build &#39;attaching_jshell&#39; artifact">
146+
<property name="artifact.temp.output.attaching_jshell" value="${artifacts.temp.dir}/attaching_jshell"/>
147+
<mkdir dir="${artifact.temp.output.attaching_jshell}"/>
148+
<jar destfile="${temp.jar.path.attaching_jshell.jar}" duplicate="preserve" filesetmanifest="mergewithoutmain">
149+
<zipfileset dir="${jshell.output.dir}"/>
150+
<zipfileset dir="${basedir}/src/main/resources/META-INF" prefix="META-INF"/>
151+
</jar>
152+
<copy file="${temp.jar.path.attaching_jshell.jar}" tofile="${artifact.temp.output.attaching_jshell}/attaching_jshell.jar"/>
153+
</target>
154+
155+
<target name="build.all.artifacts" depends="artifact.attaching_jshell" description="Build all artifacts">
156+
<mkdir dir="${artifact.output.attaching_jshell}"/>
157+
<copy todir="${artifact.output.attaching_jshell}">
158+
<fileset dir="${artifact.temp.output.attaching_jshell}"/>
159+
</copy>
160+
161+
<!-- Delete temporary files -->
162+
<delete dir="${artifacts.temp.dir}"/>
163+
</target>
164+
165+
<target name="all" depends="build.modules, build.all.artifacts" description="build all"/>
166+
</project>

Diff for: run_jshell

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
java -cp lib/attaching_jshell.jar jdk.internal.jshell.tool.JShellToolProvider --execution "attachToExistingVM:hostname(localhost),port(4568)"

Diff for: run_test_target

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
java -cp lib/attaching_jshell.jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=localhost:4568 uk.org.cinquin.attaching_jshell.ExistingVMRemoteExecutionControl
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2017, Olivier Cinquin
3+
*/
4+
5+
package uk.org.cinquin.attaching_jshell;
6+
7+
import static jdk.jshell.execution.JdiExecutionControlProvider.PARAM_HOST_NAME;
8+
import static jdk.jshell.execution.JdiExecutionControlProvider.PARAM_REMOTE_AGENT;
9+
import static jdk.jshell.execution.JdiExecutionControlProvider.PARAM_TIMEOUT;
10+
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
import jdk.jshell.spi.ExecutionControl;
15+
import jdk.jshell.spi.ExecutionControlProvider;
16+
import jdk.jshell.spi.ExecutionEnv;
17+
18+
/**
19+
* This provider is detected at runtime by {@link jdk.jshell.spi.ExecutionControl}::generate.
20+
* It needs to be advertised as a service in a META-INF/services directory to be made
21+
* available.
22+
*
23+
* Created by olivier on 4/29/17.
24+
*/
25+
public class AttachToExistingVMProvider implements ExecutionControlProvider {
26+
@Override
27+
public String name() {
28+
return "attachToExistingVM";
29+
}
30+
31+
public String PARAM_PORT = "port";
32+
33+
public Map<String,String> defaultParameters() {
34+
Map<String, String> result = new HashMap<>();
35+
result.put(PARAM_HOST_NAME, "localhost");
36+
result.put(PARAM_PORT, "4568");
37+
result.put(PARAM_TIMEOUT, "30000");
38+
return result;
39+
}
40+
41+
@Override
42+
public ExecutionControl generate(ExecutionEnv env, Map<String, String> parameters) throws Throwable {
43+
Map<String, String> dp = defaultParameters();
44+
if (parameters == null) {
45+
parameters = dp;
46+
}
47+
String remoteAgent = parameters.getOrDefault(PARAM_REMOTE_AGENT, dp.get(PARAM_REMOTE_AGENT));
48+
int timeout = Integer.parseUnsignedInt(parameters.getOrDefault(PARAM_TIMEOUT, dp.get(PARAM_TIMEOUT)));
49+
String host = parameters.getOrDefault(PARAM_HOST_NAME, dp.get(PARAM_HOST_NAME));
50+
int port = Integer.valueOf(parameters.getOrDefault(PARAM_PORT, dp.get(PARAM_PORT)));
51+
return ExistingVMJdi.create(env, remoteAgent, host, port, timeout);
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (c) 2017, Olivier Cinquin
3+
*/
4+
5+
package uk.org.cinquin.attaching_jshell;
6+
7+
import static jdk.jshell.execution.JdiExecutionControlProvider.PARAM_HOST_NAME;
8+
import static jdk.jshell.execution.JdiExecutionControlProvider.PARAM_TIMEOUT;
9+
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
13+
import jdk.jshell.JShell;
14+
15+
/**
16+
* Test class to programmatically invoke JShell in a way that it attaches to an
17+
* already-existing VM.
18+
* Created by olivier on 4/29/17.
19+
*/
20+
public class CustomJShellTest {
21+
22+
public static void main(String[] args) throws InterruptedException {
23+
JShell.Builder builder = JShell.builder();
24+
Map<String, String> params = new HashMap<>();
25+
params.put(PARAM_HOST_NAME, "localhost");
26+
params.put(PARAM_TIMEOUT, "10000");
27+
builder.executionEngine(new AttachToExistingVMProvider(), params);
28+
JShell shell = builder.build();
29+
shell.eval("int k = 3 + 15;").forEach(sne -> System.out.println(sne.toString()));
30+
shell.eval("import uk.org.cinquin.attaching_jshell.ExistingVMRemoteExecutionControl;").forEach(sne -> System.out.println(sne.toString()));
31+
shell.eval("String s = ExistingVMRemoteExecutionControl.theGoodsForTesting;").forEach(sne -> System.out.println(sne.toString()));
32+
Thread.sleep(10_000);
33+
shell.close();
34+
}
35+
36+
}

0 commit comments

Comments
 (0)