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.io.ObjectOutputStream;
  41 import java.net.ServerSocket;
  42 import java.util.ArrayList;
  43 import java.util.List;
  44 import com.sun.jdi.VMDisconnectedException;
  45 import com.sun.jdi.VirtualMachine;
  46 import jdk.jshell.VarSnippet;
  47 import jdk.jshell.execution.DirectExecutionControl;
  48 import jdk.jshell.execution.JDIExecutionControl;
  49 import jdk.jshell.execution.JDIInitiator;
  50 import jdk.jshell.execution.Util;
  51 import java.io.InputStream;
  52 import java.io.OutputStream;
  53 import java.io.PrintStream;
  54 import java.net.Socket;
  55 
  56 import java.util.HashMap;
  57 import java.util.Map;
  58 import java.util.function.Consumer;
  59 import jdk.jshell.spi.ExecutionControl;
  60 import jdk.jshell.spi.ExecutionControl.ExecutionControlException;
  61 import jdk.jshell.spi.ExecutionEnv;
  62 import static org.testng.Assert.assertEquals;
  63 import static org.testng.Assert.fail;
  64 import static jdk.jshell.execution.Util.forwardExecutionControlAndIO;
  65 import static jdk.jshell.execution.Util.remoteInput;
  66 
  67 @Test
  68 public class UserJDIUserRemoteTest extends ExecutionControlTestBase {
  69 
  70     ExecutionControl currentEC;
  71     ByteArrayOutputStream auxStream;
  72 
  73     @BeforeMethod
  74     @Override
  75     public void setUp() {
  76         auxStream = new ByteArrayOutputStream();
  77         setUp(builder -> builder.executionEngine(MyExecutionControl.create(this)));
  78     }
  79 
  80     public void testVarValue() {
  81         VarSnippet dv = varKey(assertEval("double aDouble = 1.5;"));
  82         String vd = getState().varValue(dv);
  83         assertEquals(vd, "1.5");
  84         assertEquals(auxStream.toString(), "aDouble");
  85     }
  86 
  87     public void testExtension() throws ExecutionControlException {
  88         assertEval("42;");
  89         Object res = currentEC.extensionCommand("FROG", "test");
  90         assertEquals(res, "ribbit");
  91     }
  92 
  93     public void testRedefine() {
  94         Snippet vx = varKey(assertEval("int x;"));
  95         Snippet mu = methodKey(assertEval("int mu() { return x * 4; }"));
  96         Snippet c = classKey(assertEval("class C { String v() { return \"#\" + mu(); } }"));
  97         assertEval("C c0  = new C();");
  98         assertEval("c0.v();", "\"#0\"");
  99         assertEval("int x = 10;", "10",
 100                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 101                 ste(vx, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 102         assertEval("c0.v();", "\"#40\"");
 103         assertEval("C c = new C();");
 104         assertEval("c.v();", "\"#40\"");
 105         assertEval("int mu() { return x * 3; }",
 106                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 107                 ste(mu, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 108         assertEval("c.v();", "\"#30\"");
 109         assertEval("class C { String v() { return \"@\" + mu(); } }",
 110                 ste(MAIN_SNIPPET, VALID, VALID, false, null),
 111                 ste(c, VALID, OVERWRITTEN, false, MAIN_SNIPPET));
 112         assertEval("c0.v();", "\"@30\"");
 113         assertEval("c = new C();");
 114         assertEval("c.v();", "\"@30\"");
 115         assertActiveKeys();
 116     }
 117 }
 118 
 119 class MyExecutionControl extends JDIExecutionControl {
 120 
 121     private static final String REMOTE_AGENT = MyRemoteExecutionControl.class.getName();
 122 
 123     private VirtualMachine vm;
 124     private Process process;
 125 
 126     /**
 127      * Creates an ExecutionControl instance based on a JDI
 128      * {@code LaunchingConnector}.
 129      *
 130      * @return the generator
 131      */
 132     public static ExecutionControl.Generator create(UserJDIUserRemoteTest test) {
 133         return env -> make(env, test);
 134     }
 135 
 136     /**
 137      * Creates an ExecutionControl instance based on a JDI
 138      * {@code ListeningConnector} or {@code LaunchingConnector}.
 139      *
 140      * Initialize JDI and use it to launch the remote JVM. Set-up a socket for
 141      * commands and results. This socket also transports the user
 142      * input/output/error.
 143      *
 144      * @param env the context passed by
 145          * {@link jdk.jshell.spi.ExecutionControl#start(jdk.jshell.spi.ExecutionEnv) }
 146      * @return the channel
 147      * @throws IOException if there are errors in set-up
 148      */
 149     static MyExecutionControl make(ExecutionEnv env, UserJDIUserRemoteTest test) throws IOException {
 150         try (final ServerSocket listener = new ServerSocket(0)) {
 151             // timeout after 60 seconds
 152             listener.setSoTimeout(60000);
 153             int port = listener.getLocalPort();
 154 
 155             // Set-up the JDI connection
 156             List<String> opts = new ArrayList<>(env.extraRemoteVMOptions());
 157             opts.add("-classpath");
 158             opts.add(System.getProperty("java.class.path")
 159                     + System.getProperty("path.separator")
 160                     + System.getProperty("user.dir"));
 161             JDIInitiator jdii = new JDIInitiator(port,
 162                     opts, REMOTE_AGENT, true);
 163             VirtualMachine vm = jdii.vm();
 164             Process process = jdii.process();
 165 
 166             List<Consumer<String>> deathListeners = new ArrayList<>();
 167             deathListeners.add(s -> env.closeDown());
 168             Util.detectJDIExitEvent(vm, s -> {
 169                 for (Consumer<String> h : deathListeners) {
 170                     h.accept(s);
 171                 }
 172             });
 173 
 174             // Set-up the commands/reslts on the socket.  Piggy-back snippet
 175             // output.
 176             Socket socket = listener.accept();
 177             // out before in -- match remote creation so we don't hang
 178             ObjectOutput cmdout = new ObjectOutputStream(socket.getOutputStream());
 179             Map<String, OutputStream> io = new HashMap<>();
 180             io.put("out", env.userOut());
 181             io.put("err", env.userErr());
 182             io.put("aux", test.auxStream);
 183             ObjectInput cmdin = remoteInput(socket.getInputStream(), io);
 184             MyExecutionControl myec = new MyExecutionControl(cmdout, cmdin, 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>> chans = new HashMap<>();
 259             chans.put("out", st -> System.setOut(new PrintStream(st, true)));
 260             chans.put("err", st -> System.setErr(new PrintStream(st, true)));
 261             chans.put("aux", st -> { auxPrint = new PrintStream(st, true); });
 262             forwardExecutionControlAndIO(new MyRemoteExecutionControl(), inStream, outStream, chans);
 263         } catch (Throwable ex) {
 264             throw ex;
 265         }
 266     }
 267 
 268     @Override
 269     public String varValue(String className, String varName)
 270             throws RunException, EngineTerminationException, InternalException {
 271         auxPrint.print(varName);
 272         return super.varValue(className, varName);
 273     }
 274 
 275     @Override
 276     public Object extensionCommand(String className, Object arg)
 277             throws RunException, EngineTerminationException, InternalException {
 278         if (!arg.equals("test")) {
 279             throw new InternalException("expected extensionCommand arg to be 'test' got: " + arg);
 280         }
 281         return "ribbit";
 282     }
 283 
 284 }