1 /*
   2  * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24  /*
  25  * @test
  26  * @bug 8160128 8159935
  27  * @summary Tests for Aux channel, custom remote agents, custom JDI implementations.
  28  * @build KullaTesting ExecutionControlTestBase
  29  * @run testng UserJDIUserRemoteTest
  30  */
  31 import java.io.ByteArrayOutputStream;
  32 import org.testng.annotations.Test;
  33 import org.testng.annotations.BeforeMethod;
  34 import jdk.jshell.Snippet;
  35 import static jdk.jshell.Snippet.Status.OVERWRITTEN;
  36 import static jdk.jshell.Snippet.Status.VALID;
  37 import java.io.IOException;
  38 import java.io.ObjectInput;
  39 import java.io.ObjectOutput;
  40 import java.net.ServerSocket;
  41 import java.util.ArrayList;
  42 import java.util.List;
  43 import com.sun.jdi.VMDisconnectedException;
  44 import com.sun.jdi.VirtualMachine;
  45 import jdk.jshell.VarSnippet;
  46 import jdk.jshell.execution.DirectExecutionControl;
  47 import jdk.jshell.execution.JDIExecutionControl;
  48 import jdk.jshell.execution.JDIInitiator;
  49 import jdk.jshell.execution.Util;
  50 import java.io.InputStream;
  51 import java.io.OutputStream;
  52 import java.io.PrintStream;
  53 import java.net.Socket;
  54 
  55 import java.util.HashMap;
  56 import java.util.Map;
  57 import java.util.function.Consumer;
  58 import jdk.jshell.spi.ExecutionControl;
  59 import jdk.jshell.spi.ExecutionControl.ExecutionControlException;
  60 import jdk.jshell.spi.ExecutionEnv;
  61 import static org.testng.Assert.assertEquals;
  62 import static org.testng.Assert.fail;
  63 import static jdk.jshell.execution.Util.forwardExecutionControlAndIO;
  64 import static jdk.jshell.execution.Util.remoteInputOutput;
  65 
  66 @Test
  67 public class UserJDIUserRemoteTest extends ExecutionControlTestBase {
  68 
  69     ExecutionControl currentEC;
  70     ByteArrayOutputStream auxStream;
  71 
  72     @BeforeMethod
  73     @Override
  74     public void setUp() {
  75         auxStream = new ByteArrayOutputStream();
  76         setUp(builder -> builder.executionEngine(MyExecutionControl.create(this)));
  77     }
  78 
  79     public void testVarValue() {
  80         VarSnippet dv = varKey(assertEval("double aDouble = 1.5;"));
  81         String vd = getState().varValue(dv);
  82         assertEquals(vd, "1.5");
  83         assertEquals(auxStream.toString(), "aDouble");
  84     }
  85 
  86     public void testExtension() throws ExecutionControlException {
  87         assertEval("42;");
  88         Object res = currentEC.extensionCommand("FROG", "test");
  89         assertEquals(res, "ribbit");
  90     }
  91 
  92     public void testRedefine() {
  93         Snippet vx = varKey(assertEval("int x;"));
  94         Snippet mu = methodKey(assertEval("int mu() { return x * 4; }"));
  95         Snippet c = classKey(assertEval("class C { String v() { return \"#\" + mu(); } }"));
  96         assertEval("C c0  = new C();");
  97         assertEval("c0.v();", "\"#0\"");
  98         assertEval("int x = 10;", "10",
  99                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 100                 ste(vx, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 101         assertEval("c0.v();", "\"#40\"");
 102         assertEval("C c = new C();");
 103         assertEval("c.v();", "\"#40\"");
 104         assertEval("int mu() { return x * 3; }",
 105                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 106                 ste(mu, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 107         assertEval("c.v();", "\"#30\"");
 108         assertEval("class C { String v() { return \"@\" + mu(); } }",
 109                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 110                 ste(c, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 111         assertEval("c0.v();", "\"@30\"");
 112         assertEval("c = new C();");
 113         assertEval("c.v();", "\"@30\"");
 114         assertActiveKeys();
 115     }
 116 }
 117 
 118 class MyExecutionControl extends JDIExecutionControl {
 119 
 120     private static final String REMOTE_AGENT = MyRemoteExecutionControl.class.getName();
 121 
 122     private VirtualMachine vm;
 123     private Process process;
 124 
 125     /**
 126      * Creates an ExecutionControl instance based on a JDI
 127      * {@code LaunchingConnector}.
 128      *
 129      * @return the generator
 130      */
 131     public static ExecutionControl.Generator create(UserJDIUserRemoteTest test) {
 132         return env -> make(env, test);
 133     }
 134 
 135     /**
 136      * Creates an ExecutionControl instance based on a JDI
 137      * {@code ListeningConnector} or {@code LaunchingConnector}.
 138      *
 139      * Initialize JDI and use it to launch the remote JVM. Set-up a socket for
 140      * commands and results. This socket also transports the user
 141      * input/output/error.
 142      *
 143      * @param env the context passed by
 144          * {@link jdk.jshell.spi.ExecutionControl#start(jdk.jshell.spi.ExecutionEnv) }
 145      * @return the channel
 146      * @throws IOException if there are errors in set-up
 147      */
 148     static ExecutionControl make(ExecutionEnv env, UserJDIUserRemoteTest test) throws IOException {
 149         try (final ServerSocket listener = new ServerSocket(0)) {
 150             // timeout after 60 seconds
 151             listener.setSoTimeout(60000);
 152             int port = listener.getLocalPort();
 153 
 154             // Set-up the JDI connection
 155             List<String> opts = new ArrayList<>(env.extraRemoteVMOptions());
 156             opts.add("-classpath");
 157             opts.add(System.getProperty("java.class.path")
 158                     + System.getProperty("path.separator")
 159                     + System.getProperty("user.dir"));
 160             JDIInitiator jdii = new JDIInitiator(port,
 161                     opts, REMOTE_AGENT, true);
 162             VirtualMachine vm = jdii.vm();
 163             Process process = jdii.process();
 164 
 165             List<Consumer<String>> deathListeners = new ArrayList<>();
 166             deathListeners.add(s -> env.closeDown());
 167             Util.detectJDIExitEvent(vm, s -> {
 168                 for (Consumer<String> h : deathListeners) {
 169                     h.accept(s);
 170                 }
 171             });
 172 
 173             // Set-up the commands/reslts on the socket.  Piggy-back snippet
 174             // output.
 175             Socket socket = listener.accept();
 176             // out before in -- match remote creation so we don't hang
 177             OutputStream out = socket.getOutputStream();
 178             Map<String, OutputStream> outputs = new HashMap<>();
 179             outputs.put("out", env.userOut());
 180             outputs.put("err", env.userErr());
 181             outputs.put("aux", test.auxStream);
 182             Map<String, InputStream> input = new HashMap<>();
 183             input.put("in", env.userIn());
 184             ExecutionControl myec = remoteInputOutput(socket.getInputStream(), out, outputs, input, (objIn, objOut) -> new MyExecutionControl(objOut, objIn, vm, process, deathListeners));
 185             test.currentEC = myec;
 186             return myec;
 187         }
 188     }
 189 
 190     /**
 191      * Create an instance.
 192      *
 193      * @param out the output for commands
 194      * @param in the input for responses
 195      */
 196     private MyExecutionControl(ObjectOutput out, ObjectInput in,
 197             VirtualMachine vm, Process process,
 198             List<Consumer<String>> deathListeners) {
 199         super(out, in);
 200         this.vm = vm;
 201         this.process = process;
 202         deathListeners.add(s -> disposeVM());
 203     }
 204 
 205     @Override
 206     public void close() {
 207         super.close();
 208         disposeVM();
 209     }
 210 
 211     private synchronized void disposeVM() {
 212         try {
 213             if (vm != null) {
 214                 vm.dispose(); // This could NPE, so it is caught below
 215                 vm = null;
 216             }
 217         } catch (VMDisconnectedException ex) {
 218             // Ignore if already closed
 219         } catch (Throwable e) {
 220             fail("disposeVM threw: " + e);
 221         } finally {
 222             if (process != null) {
 223                 process.destroy();
 224                 process = null;
 225             }
 226         }
 227     }
 228 
 229     @Override
 230     protected synchronized VirtualMachine vm() throws EngineTerminationException {
 231         if (vm == null) {
 232             throw new EngineTerminationException("VM closed");
 233         } else {
 234             return vm;
 235         }
 236     }
 237 
 238 }
 239 
 240 class MyRemoteExecutionControl extends DirectExecutionControl implements ExecutionControl {
 241 
 242     static PrintStream auxPrint;
 243 
 244     /**
 245      * Launch the agent, connecting to the JShell-core over the socket specified
 246      * in the command-line argument.
 247      *
 248      * @param args standard command-line arguments, expectation is the socket
 249      * number is the only argument
 250      * @throws Exception any unexpected exception
 251      */
 252     public static void main(String[] args) throws Exception {
 253         try {
 254             String loopBack = null;
 255             Socket socket = new Socket(loopBack, Integer.parseInt(args[0]));
 256             InputStream inStream = socket.getInputStream();
 257             OutputStream outStream = socket.getOutputStream();
 258             Map<String, Consumer<OutputStream>> outputs = new HashMap<>();
 259             outputs.put("out", st -> System.setOut(new PrintStream(st, true)));
 260             outputs.put("err", st -> System.setErr(new PrintStream(st, true)));
 261             outputs.put("aux", st -> { auxPrint = new PrintStream(st, true); });
 262             Map<String, Consumer<InputStream>> input = new HashMap<>();
 263             input.put("in", st -> System.setIn(st));
 264             forwardExecutionControlAndIO(new MyRemoteExecutionControl(), inStream, outStream, outputs, input);
 265         } catch (Throwable ex) {
 266             throw ex;
 267         }
 268     }
 269 
 270     @Override
 271     public String varValue(String className, String varName)
 272             throws RunException, EngineTerminationException, InternalException {
 273         auxPrint.print(varName);
 274         return super.varValue(className, varName);
 275     }
 276 
 277     @Override
 278     public Object extensionCommand(String className, Object arg)
 279             throws RunException, EngineTerminationException, InternalException {
 280         if (!arg.equals("test")) {
 281             throw new InternalException("expected extensionCommand arg to be 'test' got: " + arg);
 282         }
 283         return "ribbit";
 284     }
 285 
 286 }